/*
 Navicat MySQL Data Transfer

 Source Server         : localhost_3306
 Source Server Type    : MySQL
 Source Server Version : 50724
 Source Host           : localhost:3306
 Source Schema         : tumo

 Target Server Type    : MySQL
 Target Server Version : 50724
 File Encoding         : 65001

 Date: 03/04/2019 10:34:23
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_article
-- ----------------------------
DROP TABLE IF EXISTS `tb_article`;
CREATE TABLE `tb_article` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `title` varchar(400) DEFAULT NULL COMMENT '标题',
  `cover` varchar(400) DEFAULT NULL COMMENT '封面图片',
  `author` varchar(100) NOT NULL COMMENT '作者',
  `content` mediumtext COMMENT '内容',
  `content_md` mediumtext COMMENT '内容-Markdown',
  `category` varchar(20) DEFAULT NULL COMMENT '分类',
  `origin` varchar(100) DEFAULT NULL COMMENT '来源',
  `state` varchar(100) NOT NULL COMMENT '状态',
  `views` bigint(20) DEFAULT '0',
  `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
  `edit_time` datetime NOT NULL COMMENT '上次修改时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `type` int(11) DEFAULT '0' COMMENT '类型， 0原创 1转载',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='文章表';

-- ----------------------------
-- Records of tb_article
-- ----------------------------
BEGIN;
INSERT INTO `tb_article` VALUES (1, 'SpringBoot实现Java高并发秒杀系统之并发优化（四）', '/site/images/thumbs/1.jpg', '涂陌', '<p>之前我们已经讲了：</p>\n<ul>\n<li><p><a href=\"http://tycoding.cn/2018/10/14/seckill-web/\">SpringBoot实现Java高并发秒杀系统之Web层开发（三）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/13/seckill-service/\">SpringBoot实现Java高并发秒杀系统之Service层开发（二）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/12/seckill-dao/\">SpringBoot实现Java高并发秒杀系统之DAO层开发（一）</a></p>\n</li></ul>\n<p>今天我们来分析一下秒杀系统的难点和怎么进行并发优化。</p>\n<p>本项目的源码请参看：<a href=\"https://github.com/TyCoding/springboot-seckill\">springboot-seckill</a>  如果觉得不错可以star一下哦(#^.^#)</p>\n<!--more-->\n<p>秒杀系统架构的设计和优化分析，以我一个小菜鸡，目前是说不出来的o(╥﹏╥)o。</p>\n<p>因此呢，我这里仅从本项目已经实现的优化来介绍一下：</p>\n<p>本项目中做到了以下优化：</p>\n<ul>\n<li>秒杀接口采用md5加密方式防刷。</li><li>订单表使用联合主键方式，限制一个用户只能购买该商品一次。</li><li>配合Spring事务控制实现简单的优化。</li><li>使用redis缓存优化。</li></ul>\n<h1 id=\"h1-spring-\"><a name=\"Spring的事务控制\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring的事务控制</h1><p>Spring的声明式事务通过：传播行为、隔离级别、只读提示、事务超时、回滚规则来进行定义。</p>\n<h2 id=\"h2-u4F20u64ADu884Cu4E3A\"><a name=\"传播行为\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>传播行为</h2><p>事务的第一个方面就是传播行为。传播行为定义了客户端与被调用方法之间的事务边界。Spring定义了7中不同的传播行为，传播规则规定了何时要创建一个事务或何时使用已有的事务：</p>\n<table>\n<thead>\n<tr>\n<th>传播行为</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>PROPAGATION_MANDATORY</td>\n<td>表示该方法必须在事务中运行。如果当前事务不存在，则会抛出一个异常</td>\n</tr>\n<tr>\n<td>PROPAGATION_NESTED</td>\n<td>表示如果当前已经存在一个事务，那么该方法将会在嵌套事务中运行。嵌套的事务可以独立与当前事务进行单独的提交或回滚</td>\n</tr>\n<tr>\n<td>PROPAGATION_NEVER</td>\n<td>表示当前方法不应该运行在事务上下文中，如果当前正在有一个事务运行，则会抛出异常</td>\n</tr>\n<tr>\n<td>PROPAGATION_NOT_SUPPORTED</td>\n<td>表示该方法不应该运行在事务中。</td>\n</tr>\n<tr>\n<td>PROPAGATION_REQUIRED</td>\n<td>表示当前方法必须运行在事务中。如果当前事务存在，方法将会在该事务中运行。否者，会启动一个新的事务</td>\n</tr>\n<tr>\n<td>PROPAGATION_REQUIRES_NEW</td>\n<td>表示当前方法必须运行在它自己的事务中。一个新的事务将被启动</td>\n</tr>\n<tr>\n<td>PROPAGATION_SUPPORTS</td>\n<td>表示当前方法不需要事务上下文，但是如果存在当前事务的话，那么该方法会在这个事务中运行</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"h2-u9694u79BBu7EA7u522B\"><a name=\"隔离级别\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>隔离级别</h2><p>声明式事务的第二个维度就是隔离级别。隔离级别定义了一个事务可能受其他并发事务影响的程度。多个事务并发运行，经常会操作相同的数据来完成各自的任务，但是可以回导致以下问题：</p>\n<ul>\n<li>更新丢失：当多个事务选择同一行操作，并且都是基于最初的选定的值，由于每个事务都不知道其他事务的存在，就会发生更新覆盖的问题。</li><li>脏读：事务A读取了事务B已经修改但为提交的数据。若事务B回滚数据，事务A的数据存在不一致的问题。</li><li>不可重复读：书屋A第一次读取最初数据，第二次读取事务B已经提交的修改或删除的数据。导致两次数据读取不一致。不符合事务的隔离性。</li><li>幻读：事务A根据相同条件第二次查询到的事务B提交的新增数据，两次数据结果不一致，不符合事务的隔离性。</li></ul>\n<p>理想情况下，事务之间是完全隔离的，从而可以防止这些问题的发生。但是完全的隔离会导致性能问题，因为它通常会涉及锁定数据库中的记录。侵占性的锁定会阻碍并发性，要求事务互相等待以完成各自的工作。</p>\n<p>因此为了实现在事务隔离上有一定的灵活性。因此，就会有多重隔离级别：</p>\n<table>\n<thead>\n<tr>\n<th>隔离级别</th>\n<th>含义</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>ISOLATION_DEFAULT</td>\n<td>使用后端数据库默认的隔离级别</td>\n</tr>\n<tr>\n<td>SIOLATION_READ_UNCOMMITTED</td>\n<td>允许读取尚未提交的数据变更。可能会导致脏读、幻读或不可重复读</td>\n</tr>\n<tr>\n<td>ISOLATION_READ_COMMITTED</td>\n<td>允许读取并发事务提交的数据。可以阻止脏读，但是幻读或不可重复读仍可能发生</td>\n</tr>\n<tr>\n<td>ISOLATION_REPEATABLE_READ</td>\n<td>对同一字段的多次读取结果是一致的，除非数据是被本事务自己所修改，可以阻止脏读和不可重复读，但幻读仍可能发生</td>\n</tr>\n<tr>\n<td>ISOLATION_SERIALIZABLE</td>\n<td>完全服从ACID的事务隔离级别，确保阻止脏读、不可重复读、幻读。这是最慢的事务隔离级别，因为它通常是通过完全锁定事务相关的数据库来实现的</td>\n</tr>\n</tbody>\n</table>\n<h2 id=\"h2-u56DEu6EDAu89C4u5219\"><a name=\"回滚规则\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>回滚规则</h2><p>Spring的事务管理器默认是针对unchecked exception回滚，也就是默认对Error异常和RuntimeException异常以及其子类进行事务回滚。</p>\n<p>也就是说事务只有在遇到运行期异常才会回滚，而在遇到检查型异常时不会回滚。</p>\n<p>这也就是我们之前设计Service业务层逻辑的时候一再强调捕获<code>try catch</code>异常，且将编译期异常转换为运行期异常。</p>\n<h1 id=\"h1-u7B80u5355u4F18u5316\"><a name=\"简单优化\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>简单优化</h1><p>这里我们还是要关注一些项目中的两个核心的业务：1.减库存；2.插入购买明细。我们以一张图来看一下这两个操作的事务执行流程：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-fc65c8e1cf146031.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>可以看到我们的秒杀操作主要是基于Mysql的事务进行的，而基于MySQL事务的秒杀操作主要瓶颈是网络延迟和GC（Java垃圾回收机制）。执行一条update语句首先要拿到MySQL的行级锁rowLock，而我们要解决的就是如何降低update对rowLock的持有时间。</p>\n<p>我们先了解一下MySQL的InnoDB储存引擎的行级锁（rowLock）:</p>\n<ul>\n<li>行锁的劣势：开销大；加锁慢；会出现死锁</li><li>行锁的优势：锁的粒度小，发生锁冲突的概率低；处理并发的能力强</li><li>加锁的方式：自动加锁。对于UPDATE、DELETE和INSERT语句，InnoDB会自动给涉及数据集加排他锁；对于普通SELECT语句，InnoDB不会加任何锁；当然我们也可以显示的加锁：</li><li>共享锁：select * from tableName where … + lock in share more</li><li>排他锁：select * from tableName where … + for update</li><li>InnoDB和MyISAM的最大不同点有两个：一，InnoDB支持事务(transaction)；二，默认采用行级锁。加锁可以保证事务的一致性，可谓是有人(锁)的地方，就有江湖(事务)。</li></ul>\n<p>详细的介绍请看博文：<a href=\"http://www.cnblogs.com/itdragon/p/8194622.html\">MySQL 表锁和行锁机制</a></p>\n<p>所以在此基础上我们可以进行简单的优化：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-3cb78bf88dca2f38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>很简单，就是调整update和insert操作的执行顺序。目的就是为了缩短update对rowLock的持有时间提高性能，因为我们的查询语句使用了<code>insert ignore into xx</code>的方式来避免重复秒杀，那么闲执行insert语句可以在插入时就排除可能存在重复秒杀的操作，这样就不用再向下执行更新操作了。在一定程度上降低了一倍的rowLock持有时间。</p>\n<p>下面是源码：</p>\n<pre><code class=\"lang-java\">@Override\n@Transactional\npublic SeckillExecution executeSeckill(long seckillId, BigDecimal money, long userPhone, String md5)\n        throws SeckillException, RepeatKillException, SeckillCloseException {\n    if (md5 == null || !md5.equals(getMD5(seckillId))) {\n        throw new SeckillException(&quot;seckill data rewrite&quot;);\n    }\n    //执行秒杀逻辑：1.减库存；2.储存秒杀订单\n    Date nowTime = new Date();\n\n    try {\n        //记录秒杀订单信息\n        int insertCount = seckillOrderMapper.insertOrder(seckillId, money, userPhone);\n        //唯一性：seckillId,userPhone，保证一个用户只能秒杀一件商品\n        if (insertCount &lt;= 0) {\n            //重复秒杀\n            throw new RepeatKillException(&quot;seckill repeated&quot;);\n        } else {\n            //减库存\n            int updateCount = seckillMapper.reduceStock(seckillId, nowTime);\n            if (updateCount &lt;= 0) {\n                //没有更新记录，秒杀结束\n                throw new SeckillCloseException(&quot;seckill is closed&quot;);\n            } else {\n                //秒杀成功\n                SeckillOrder seckillOrder = seckillOrderMapper.findById(seckillId);\n                return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, seckillOrder);\n            }\n        }\n    } catch (SeckillCloseException e) {\n        throw e;\n    } catch (RepeatKillException e) {\n        throw e;\n    } catch (Exception e) {\n        logger.error(e.getMessage(), e);\n        //所有编译期异常，转换为运行期异常\n        throw new SeckillException(&quot;seckill inner error:&quot; + e.getMessage());\n    }\n}\n</code></pre>\n<p><br/></p>\n<h1 id=\"h1-redis-\"><a name=\"Redis缓存优化\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Redis缓存优化</h1><h2 id=\"h2-u51C6u5907\"><a name=\"准备\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>准备</h2><p>如果想使用Redis缓存进行优化，首先你需要连接什么是Redis缓存，以及Spring提供的一种操作Redis缓存的框架：Spring-data-redis。最终要的是：你需要在本地电脑上安装好Redis缓存服务器：</p>\n<p>所以呢，我推荐你看一下我的几篇文章：</p>\n<ul>\n<li><p><a href=\"http://tycoding.cn/2018/09/24/redis/\">Redis即Spring-data-redis入门学习</a></p>\n</li><li><p><a href=\"https://github.com/TyCoding/ssm-redis-solr\">优雅的整合SSM+Shiro+Redis+Solr框架</a></p>\n</li></ul>\n<p>在看了上面的文章后相信你已经初步了解了使用Spring-data-redis操作Redis缓存服务器，下面讲解针对本项目的缓存优化实现：</p>\n<p>启动安装好的Redis缓存服务器，修改项目中的 <a href=\"https://github.com/TyCoding/springboot-seckill/blob/master/src/main/resources/application.yml\">resources/application.yml</a> 关于Redis和Jedis的配置，</p>\n<p>例中我使用的本地Redis服务器：host：127.0.0.1；port：6379</p>\n<h2 id=\"h2--redis-jedis-\"><a name=\"添加Redis、Jedis缓存配置\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>添加Redis、Jedis缓存配置</h2><p>这里我们依赖:</p>\n<pre><code class=\"lang-xml\">        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- redis客户端 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;redis.clients&lt;/groupId&gt;\n            &lt;artifactId&gt;jedis&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n</code></pre>\n<p>同时我们需要在<code>application.yml</code>中配置缓存：</p>\n<pre><code class=\"lang-xml\">  #redis缓存\n  redis:\n    #redis数据库索引，默认是0\n    database: 0\n    #redis服务器地址，这里用本地的redis\n    host: 127.0.0.1\n    # Redis服务器连接密码（默认为空）\n    password:\n    #redis服务器连接端口，默认是6379\n    port: 6379\n    # 连接超时时间（毫秒）\n    timeout: 1000\n    jedis:\n      pool:\n        # 连接池最大连接数（使用负值表示没有限制）\n        max-active: 8\n        # 连接池最大阻塞等待时间（使用负值表示没有限制\n        max-wait: -1\n        # 连接池中的最大空闲连接\n        max-idle: 8\n        # 连接池中的最小空闲连接\n        min-idle: 0\n</code></pre>\n<h3 id=\"h3--redis-\"><a name=\"实现Redis的序列化\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现Redis的序列化</h3><blockquote>\n<p>1.创建JedisConfig</p>\n</blockquote>\n<pre><code class=\"lang-java\">@Configuration\npublic class JedisConfig {\n    private Logger logger = LoggerFactory.getLogger(JedisConfig.class);\n\n    @Value(&quot;${spring.redis.host}&quot;)\n    private String host;\n\n    @Value(&quot;${spring.redis.port}&quot;)\n    private int port;\n\n    @Value(&quot;${spring.redis.timeout}&quot;)\n    private int timeout;\n\n    @Value(&quot;${spring.redis.jedis.pool.max-active}&quot;)\n    private int maxActive;\n\n    @Value(&quot;${spring.redis.jedis.pool.max-idle}&quot;)\n    private int maxIdle;\n\n    @Value(&quot;${spring.redis.jedis.pool.min-idle}&quot;)\n    private int minIdle;\n\n    @Value(&quot;${spring.redis.jedis.pool.max-wait}&quot;)\n    private long maxWaitMillis;\n\n    @Bean\n    public JedisPool redisPoolFactory(){\n        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();\n        jedisPoolConfig.setMaxIdle(maxIdle);\n        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);\n        jedisPoolConfig.setMaxTotal(maxActive);\n        jedisPoolConfig.setMinIdle(minIdle);\n        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);\n\n        logger.info(&quot;JedisPool注入成功&quot;);\n        logger.info(&quot;redis地址：&quot; + host + &quot;:&quot; + port);\n        return jedisPool;\n    }\n}\n</code></pre>\n<p>这里是为了将我们在<code>application.yml</code>中配置的参数注入到JedisPool中，使用Spring的<code><a href=\"https://github.com/Value\" title=\"&#64;Value\" class=\"at-link\">@Value</a></code>注解能读取到Spring配置文件中已经配置的参数的值</p>\n<blockquote>\n<p>2.创建RedisTemplateConfig</p>\n</blockquote>\n<pre><code class=\"lang-java\">@Configuration\npublic class RedisTemplateConfig {\n\n    private final Logger logger = LoggerFactory.getLogger(this.getClass());\n\n    @Bean\n    public RedisTemplate&lt;String, Object&gt; redisTemplate(RedisConnectionFactory redisConnectionFactory){\n        Jackson2JsonRedisSerializer&lt;Object&gt; jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer&lt;Object&gt;(Object.class);\n        ObjectMapper objectMapper = new ObjectMapper();\n        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);\n        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);\n        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);\n        RedisTemplate&lt;String, Object&gt; redisTemplate = new RedisTemplate&lt;String, Object&gt;();\n        redisTemplate.setConnectionFactory(redisConnectionFactory);\n        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);\n        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);\n        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);\n        redisTemplate.afterPropertiesSet();\n        logger.info(&quot;RedisTemplate序列化配置，转化方式：&quot; + jackson2JsonRedisSerializer.getClass().getName());\n        return redisTemplate;\n    }\n}\n</code></pre>\n<p>这一步才是真正实现Redis序列化的配置，当然，不实现序列化也是可以的，舍去上面两步，我们依然可以将数据放入到Redis缓存中。所以我们需要注意以下几点：</p>\n<ul>\n<li><p>实现序列化目前而言不是必须的，因为我们使用了Spring-data-redis提供的高度封装的RedisTemplate模板类。</p>\n</li><li><p>SpringBoot2.x实现Redis的序列化仍是由很多方案，但是我这里使用了Spring-data-redis提供的一种jackson2JsonRedisSerializer的序列化方式。</p>\n</li><li><p>如果不实现Redis的序列化，可以往Redis中存入数据，但是存入的key都是乱码的，想要避免这一点就必须实现序列化。</p>\n</li><li><p>这个步骤和我们之前<a href=\"https://github.com/TyCoding/ssm-redis-solr\">整合SSM+Redis+Shiro+Solr框架</a>中已经讲到了用XML实现序列化配置，这里仅是换成了Java配置而已。</p>\n</li></ul>\n<h2 id=\"h2--findall-\"><a name=\"优化findAll方法\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>优化findAll方法</h2><p>秒杀列表（即查询<code>findAll</code>方法）也是经常被访问的，所以我们可以将商品数据放入Redis缓存中</p>\n<p>调用<code>findAll()</code>方法得到的是一个List集合，而我们不能直接将一个List集合的数据放入缓存(<code>key-value</code>形式)中，我们必须指定key和value的为实体类中某个属性值。</p>\n<p>所以本例中我们采用key: 秒杀商品ID值；value：秒杀商品数据（实体类）。</p>\n<pre><code class=\"lang-java\">    //设置秒杀redis缓存的key\n    private final String key = &quot;seckill&quot;;\n\n    @Override\n    public List&lt;Seckill&gt; findAll() {\n        List&lt;Seckill&gt; seckillList = redisTemplate.boundHashOps(&quot;seckill&quot;).values();\n        if (seckillList == null || seckillList.size() == 0){\n            //说明缓存中没有秒杀列表数据\n            //查询数据库中秒杀列表数据，并将列表数据循环放入redis缓存中\n            seckillList = seckillMapper.findAll();\n            for (Seckill seckill : seckillList){\n                //将秒杀列表数据依次放入redis缓存中，key:秒杀表的ID值；value:秒杀商品数据\n                redisTemplate.boundHashOps(key).put(seckill.getSeckillId(), seckill);\n                logger.info(&quot;findAll -&gt; 从数据库中读取放入缓存中&quot;);\n            }\n        }else{\n            logger.info(&quot;findAll -&gt; 从缓存中读取&quot;);\n        }\n        return seckillList;\n    }\n</code></pre>\n<h2 id=\"h2--exportseckillurl-\"><a name=\"优化exportSeckillUrl方法\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>优化exportSeckillUrl方法</h2><p><code>exportSeckillUrl()</code>暴露接口的方法也是常调用的，因为其中要频繁的调用<code>findById()</code>方法，所以将指定ID的商品数据放入缓存中也是很必要的，当然我们在之前的findAll()方法中已经将每个ID的数据都分别放入了缓存中。</p>\n<pre><code class=\"lang-java\">    @Override\n    public Exposer exportSeckillUrl(long seckillId) {\n        Seckill seckill = (Seckill) redisTemplate.boundHashOps(key).get(seckillId);\n        if (seckill == null) {\n            //说明redis缓存中没有此key对应的value\n            //查询数据库，并将数据放入缓存中\n            seckill = seckillMapper.findById(seckillId);\n            if (seckill == null) {\n                //说明没有查询到\n                return new Exposer(false, seckillId);\n            } else {\n                //查询到了，存入redis缓存中。 key:秒杀表的ID值； value:秒杀表数据\n                redisTemplate.boundHashOps(key).put(seckill.getSeckillId(), seckill);\n                logger.info(&quot;RedisTemplate -&gt; 从数据库中读取并放入缓存中&quot;);\n            }\n        } else {\n            logger.info(&quot;RedisTemplate -&gt; 从缓存中读取&quot;);\n        }\n        Date startTime = seckill.getStartTime();\n        Date endTime = seckill.getEndTime();\n        //获取系统时间\n        Date nowTime = new Date();\n        if (nowTime.getTime() &lt; startTime.getTime() || nowTime.getTime() &gt; endTime.getTime()) {\n            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());\n        }\n        //转换特定字符串的过程，不可逆的算法\n        String md5 = getMD5(seckillId);\n        return new Exposer(true, md5, seckillId);\n    }\n</code></pre>\n<h2 id=\"h2--executeseckill-\"><a name=\"优化executeSeckill方法\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>优化executeSeckill方法</h2><p>上面的两个查询操作都将商品数据放入了缓存中，key:商品ID；value:商品数据（实体类）。而对于减库存操作，用户每次抢购一件商品，商品的库存总量都需要-1，但是我们页面展示的数据都是从缓存中读取的，即使修改了数据库中的库存数量，页面上展示的数量仍是无法修改的，所以我们同时要修改缓存中的数据来保证缓存和数据库数据的一致性。</p>\n<p>更新缓存的办法就简单了，即重新<code>put</code>进去数据就行了，因为Redis缓存数据库中存放的数据是key-value形式，你重新对指定key put进去新的值，就势必会覆盖掉原来的值（这也就是我们为什么设计key:商品ID；value:商品数据）。</p>\n<pre><code class=\"lang-java\">@Override\n    @Transactional\n    public SeckillExecution executeSeckill(long seckillId, BigDecimal money, long userPhone, String md5)\n            throws SeckillException, RepeatKillException, SeckillCloseException {\n        if (md5 == null || !md5.equals(getMD5(seckillId))) {\n            throw new SeckillException(&quot;seckill data rewrite&quot;);\n        }\n        //执行秒杀逻辑：1.减库存；2.储存秒杀订单\n        Date nowTime = new Date();\n\n        try {\n            //记录秒杀订单信息\n            int insertCount = seckillOrderMapper.insertOrder(seckillId, money, userPhone);\n            //唯一性：seckillId,userPhone，保证一个用户只能秒杀一件商品\n            if (insertCount &lt;= 0) {\n                //重复秒杀\n                throw new RepeatKillException(&quot;seckill repeated&quot;);\n            } else {\n                //减库存\n                int updateCount = seckillMapper.reduceStock(seckillId, nowTime);\n                if (updateCount &lt;= 0) {\n                    //没有更新记录，秒杀结束\n                    throw new SeckillCloseException(&quot;seckill is closed&quot;);\n                } else {\n                    //秒杀成功\n                    SeckillOrder seckillOrder = seckillOrderMapper.findById(seckillId);\n\n                    //更新缓存（更新库存数量）\n                    Seckill seckill = (Seckill) redisTemplate.boundHashOps(key).get(seckillId);\n                    seckill.setStockCount(seckill.getSeckillId() - 1);\n                    redisTemplate.boundHashOps(key).put(seckillId, seckill);\n\n                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, seckillOrder);\n                }\n            }\n        } catch (SeckillCloseException e) {\n            throw e;\n        } catch (RepeatKillException e) {\n            throw e;\n        } catch (Exception e) {\n            logger.error(e.getMessage(), e);\n            //所有编译期异常，转换为运行期异常\n            throw new SeckillException(&quot;seckill inner error:&quot; + e.getMessage());\n        }\n    }\n</code></pre>\n<p><br/></p>\n<h1 id=\"h1-u4EA4u6D41\"><a name=\"交流\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>交流</h1><p>如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！</p>\n<p><br/></p>\n<h1 id=\"h1-u8054u7CFB\"><a name=\"联系\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>联系</h1><p>If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.</p>\n<ul>\n<li><a href=\"http://www.tycoding.cn\"\">Blog&#64;TyCoding’s blog</a></li><li><a href=\"https://github.com/TyCoding\"\">GitHub&#64;TyCoding</a></li><li><a href=\"https://www.zhihu.com/people/tomo-83-82/activities\"\">ZhiHu&#64;TyCoding</a></li></ul>\n', '\n之前我们已经讲了：\n\n* [SpringBoot实现Java高并发秒杀系统之Web层开发（三）](http://tycoding.cn/2018/10/14/seckill-web/)\n\n* [SpringBoot实现Java高并发秒杀系统之Service层开发（二）](http://tycoding.cn/2018/10/13/seckill-service/)\n\n* [SpringBoot实现Java高并发秒杀系统之DAO层开发（一）](http://tycoding.cn/2018/10/12/seckill-dao/)\n\n今天我们来分析一下秒杀系统的难点和怎么进行并发优化。\n\n本项目的源码请参看：[springboot-seckill](https://github.com/TyCoding/springboot-seckill)  如果觉得不错可以star一下哦(#^.^#)\n\n<!--more-->\n\n\n秒杀系统架构的设计和优化分析，以我一个小菜鸡，目前是说不出来的o(╥﹏╥)o。\n\n因此呢，我这里仅从本项目已经实现的优化来介绍一下：\n\n本项目中做到了以下优化：\n\n* 秒杀接口采用md5加密方式防刷。\n* 订单表使用联合主键方式，限制一个用户只能购买该商品一次。\n* 配合Spring事务控制实现简单的优化。\n* 使用redis缓存优化。\n\n# Spring的事务控制\n\nSpring的声明式事务通过：传播行为、隔离级别、只读提示、事务超时、回滚规则来进行定义。\n\n## 传播行为\n\n事务的第一个方面就是传播行为。传播行为定义了客户端与被调用方法之间的事务边界。Spring定义了7中不同的传播行为，传播规则规定了何时要创建一个事务或何时使用已有的事务：\n\n| 传播行为 | 含义 |\n| -- | -- | \n| PROPAGATION_MANDATORY | 表示该方法必须在事务中运行。如果当前事务不存在，则会抛出一个异常 |\n| PROPAGATION_NESTED | 表示如果当前已经存在一个事务，那么该方法将会在嵌套事务中运行。嵌套的事务可以独立与当前事务进行单独的提交或回滚 |\n| PROPAGATION_NEVER | 表示当前方法不应该运行在事务上下文中，如果当前正在有一个事务运行，则会抛出异常 |\n| PROPAGATION_NOT_SUPPORTED | 表示该方法不应该运行在事务中。 |\n| PROPAGATION_REQUIRED | 表示当前方法必须运行在事务中。如果当前事务存在，方法将会在该事务中运行。否者，会启动一个新的事务 | \n| PROPAGATION_REQUIRES_NEW | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动 |\n| PROPAGATION_SUPPORTS | 表示当前方法不需要事务上下文，但是如果存在当前事务的话，那么该方法会在这个事务中运行 |\n\n## 隔离级别\n\n声明式事务的第二个维度就是隔离级别。隔离级别定义了一个事务可能受其他并发事务影响的程度。多个事务并发运行，经常会操作相同的数据来完成各自的任务，但是可以回导致以下问题：\n\n* 更新丢失：当多个事务选择同一行操作，并且都是基于最初的选定的值，由于每个事务都不知道其他事务的存在，就会发生更新覆盖的问题。\n* 脏读：事务A读取了事务B已经修改但为提交的数据。若事务B回滚数据，事务A的数据存在不一致的问题。\n* 不可重复读：书屋A第一次读取最初数据，第二次读取事务B已经提交的修改或删除的数据。导致两次数据读取不一致。不符合事务的隔离性。\n* 幻读：事务A根据相同条件第二次查询到的事务B提交的新增数据，两次数据结果不一致，不符合事务的隔离性。\n\n理想情况下，事务之间是完全隔离的，从而可以防止这些问题的发生。但是完全的隔离会导致性能问题，因为它通常会涉及锁定数据库中的记录。侵占性的锁定会阻碍并发性，要求事务互相等待以完成各自的工作。\n\n因此为了实现在事务隔离上有一定的灵活性。因此，就会有多重隔离级别：\n\n| 隔离级别 | 含义 |\n| -- | -- |\n| ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | \n| SIOLATION_READ_UNCOMMITTED | 允许读取尚未提交的数据变更。可能会导致脏读、幻读或不可重复读 | \n| ISOLATION_READ_COMMITTED | 允许读取并发事务提交的数据。可以阻止脏读，但是幻读或不可重复读仍可能发生 | \n| ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果是一致的，除非数据是被本事务自己所修改，可以阻止脏读和不可重复读，但幻读仍可能发生 | \n| ISOLATION_SERIALIZABLE | 完全服从ACID的事务隔离级别，确保阻止脏读、不可重复读、幻读。这是最慢的事务隔离级别，因为它通常是通过完全锁定事务相关的数据库来实现的 |\n\n## 回滚规则\n\nSpring的事务管理器默认是针对unchecked exception回滚，也就是默认对Error异常和RuntimeException异常以及其子类进行事务回滚。\n\n也就是说事务只有在遇到运行期异常才会回滚，而在遇到检查型异常时不会回滚。\n\n这也就是我们之前设计Service业务层逻辑的时候一再强调捕获`try catch`异常，且将编译期异常转换为运行期异常。\n\n\n# 简单优化\n\n这里我们还是要关注一些项目中的两个核心的业务：1.减库存；2.插入购买明细。我们以一张图来看一下这两个操作的事务执行流程：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-fc65c8e1cf146031.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n可以看到我们的秒杀操作主要是基于Mysql的事务进行的，而基于MySQL事务的秒杀操作主要瓶颈是网络延迟和GC（Java垃圾回收机制）。执行一条update语句首先要拿到MySQL的行级锁rowLock，而我们要解决的就是如何降低update对rowLock的持有时间。\n\n我们先了解一下MySQL的InnoDB储存引擎的行级锁（rowLock）:\n\n* 行锁的劣势：开销大；加锁慢；会出现死锁\n* 行锁的优势：锁的粒度小，发生锁冲突的概率低；处理并发的能力强\n* 加锁的方式：自动加锁。对于UPDATE、DELETE和INSERT语句，InnoDB会自动给涉及数据集加排他锁；对于普通SELECT语句，InnoDB不会加任何锁；当然我们也可以显示的加锁：\n* 共享锁：select * from tableName where ... + lock in share more\n* 排他锁：select * from tableName where ... + for update\n* InnoDB和MyISAM的最大不同点有两个：一，InnoDB支持事务(transaction)；二，默认采用行级锁。加锁可以保证事务的一致性，可谓是有人(锁)的地方，就有江湖(事务)。\n\n详细的介绍请看博文：[MySQL 表锁和行锁机制](http://www.cnblogs.com/itdragon/p/8194622.html)\n\n所以在此基础上我们可以进行简单的优化：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-3cb78bf88dca2f38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n很简单，就是调整update和insert操作的执行顺序。目的就是为了缩短update对rowLock的持有时间提高性能，因为我们的查询语句使用了`insert ignore into xx`的方式来避免重复秒杀，那么闲执行insert语句可以在插入时就排除可能存在重复秒杀的操作，这样就不用再向下执行更新操作了。在一定程度上降低了一倍的rowLock持有时间。\n\n下面是源码：\n\n```java\n@Override\n@Transactional\npublic SeckillExecution executeSeckill(long seckillId, BigDecimal money, long userPhone, String md5)\n        throws SeckillException, RepeatKillException, SeckillCloseException {\n    if (md5 == null || !md5.equals(getMD5(seckillId))) {\n        throw new SeckillException(\"seckill data rewrite\");\n    }\n    //执行秒杀逻辑：1.减库存；2.储存秒杀订单\n    Date nowTime = new Date();\n\n    try {\n        //记录秒杀订单信息\n        int insertCount = seckillOrderMapper.insertOrder(seckillId, money, userPhone);\n        //唯一性：seckillId,userPhone，保证一个用户只能秒杀一件商品\n        if (insertCount <= 0) {\n            //重复秒杀\n            throw new RepeatKillException(\"seckill repeated\");\n        } else {\n            //减库存\n            int updateCount = seckillMapper.reduceStock(seckillId, nowTime);\n            if (updateCount <= 0) {\n                //没有更新记录，秒杀结束\n                throw new SeckillCloseException(\"seckill is closed\");\n            } else {\n                //秒杀成功\n                SeckillOrder seckillOrder = seckillOrderMapper.findById(seckillId);\n                return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, seckillOrder);\n            }\n        }\n    } catch (SeckillCloseException e) {\n        throw e;\n    } catch (RepeatKillException e) {\n        throw e;\n    } catch (Exception e) {\n        logger.error(e.getMessage(), e);\n        //所有编译期异常，转换为运行期异常\n        throw new SeckillException(\"seckill inner error:\" + e.getMessage());\n    }\n}\n```\n\n<br/>\n\n# Redis缓存优化\n\n## 准备\n\n如果想使用Redis缓存进行优化，首先你需要连接什么是Redis缓存，以及Spring提供的一种操作Redis缓存的框架：Spring-data-redis。最终要的是：你需要在本地电脑上安装好Redis缓存服务器：\n\n所以呢，我推荐你看一下我的几篇文章：\n\n* [Redis即Spring-data-redis入门学习](http://tycoding.cn/2018/09/24/redis/)\n\n* [优雅的整合SSM+Shiro+Redis+Solr框架](https://github.com/TyCoding/ssm-redis-solr)\n\n在看了上面的文章后相信你已经初步了解了使用Spring-data-redis操作Redis缓存服务器，下面讲解针对本项目的缓存优化实现：\n\n启动安装好的Redis缓存服务器，修改项目中的 [resources/application.yml](https://github.com/TyCoding/springboot-seckill/blob/master/src/main/resources/application.yml) 关于Redis和Jedis的配置，\n\n例中我使用的本地Redis服务器：host：127.0.0.1；port：6379\n\n## 添加Redis、Jedis缓存配置\n\n这里我们依赖:\n\n```xml\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n        \n        <!-- redis客户端 -->\n        <dependency>\n            <groupId>redis.clients</groupId>\n            <artifactId>jedis</artifactId>\n        </dependency>\n```\n\n同时我们需要在`application.yml`中配置缓存：\n\n```xml\n  #redis缓存\n  redis:\n    #redis数据库索引，默认是0\n    database: 0\n    #redis服务器地址，这里用本地的redis\n    host: 127.0.0.1\n    # Redis服务器连接密码（默认为空）\n    password:\n    #redis服务器连接端口，默认是6379\n    port: 6379\n    # 连接超时时间（毫秒）\n    timeout: 1000\n    jedis:\n      pool:\n        # 连接池最大连接数（使用负值表示没有限制）\n        max-active: 8\n        # 连接池最大阻塞等待时间（使用负值表示没有限制\n        max-wait: -1\n        # 连接池中的最大空闲连接\n        max-idle: 8\n        # 连接池中的最小空闲连接\n        min-idle: 0\n```\n\n### 实现Redis的序列化\n\n> 1.创建JedisConfig\n\n```java\n@Configuration\npublic class JedisConfig {\n    private Logger logger = LoggerFactory.getLogger(JedisConfig.class);\n\n    @Value(\"${spring.redis.host}\")\n    private String host;\n\n    @Value(\"${spring.redis.port}\")\n    private int port;\n\n    @Value(\"${spring.redis.timeout}\")\n    private int timeout;\n\n    @Value(\"${spring.redis.jedis.pool.max-active}\")\n    private int maxActive;\n\n    @Value(\"${spring.redis.jedis.pool.max-idle}\")\n    private int maxIdle;\n\n    @Value(\"${spring.redis.jedis.pool.min-idle}\")\n    private int minIdle;\n\n    @Value(\"${spring.redis.jedis.pool.max-wait}\")\n    private long maxWaitMillis;\n\n    @Bean\n    public JedisPool redisPoolFactory(){\n        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();\n        jedisPoolConfig.setMaxIdle(maxIdle);\n        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);\n        jedisPoolConfig.setMaxTotal(maxActive);\n        jedisPoolConfig.setMinIdle(minIdle);\n        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);\n\n        logger.info(\"JedisPool注入成功\");\n        logger.info(\"redis地址：\" + host + \":\" + port);\n        return jedisPool;\n    }\n}\n```\n\n这里是为了将我们在`application.yml`中配置的参数注入到JedisPool中，使用Spring的`@Value`注解能读取到Spring配置文件中已经配置的参数的值\n\n> 2.创建RedisTemplateConfig\n\n```java\n@Configuration\npublic class RedisTemplateConfig {\n\n    private final Logger logger = LoggerFactory.getLogger(this.getClass());\n\n    @Bean\n    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){\n        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);\n        ObjectMapper objectMapper = new ObjectMapper();\n        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);\n        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);\n        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);\n        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();\n        redisTemplate.setConnectionFactory(redisConnectionFactory);\n        redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);\n        redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);\n        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);\n        redisTemplate.afterPropertiesSet();\n        logger.info(\"RedisTemplate序列化配置，转化方式：\" + jackson2JsonRedisSerializer.getClass().getName());\n        return redisTemplate;\n    }\n}\n```\n\n这一步才是真正实现Redis序列化的配置，当然，不实现序列化也是可以的，舍去上面两步，我们依然可以将数据放入到Redis缓存中。所以我们需要注意以下几点：\n\n* 实现序列化目前而言不是必须的，因为我们使用了Spring-data-redis提供的高度封装的RedisTemplate模板类。\n\n* SpringBoot2.x实现Redis的序列化仍是由很多方案，但是我这里使用了Spring-data-redis提供的一种jackson2JsonRedisSerializer的序列化方式。\n\n* 如果不实现Redis的序列化，可以往Redis中存入数据，但是存入的key都是乱码的，想要避免这一点就必须实现序列化。\n\n* 这个步骤和我们之前[整合SSM+Redis+Shiro+Solr框架](https://github.com/TyCoding/ssm-redis-solr)中已经讲到了用XML实现序列化配置，这里仅是换成了Java配置而已。\n\n## 优化findAll方法\n\n秒杀列表（即查询`findAll`方法）也是经常被访问的，所以我们可以将商品数据放入Redis缓存中\n\n调用`findAll()`方法得到的是一个List集合，而我们不能直接将一个List集合的数据放入缓存(`key-value`形式)中，我们必须指定key和value的为实体类中某个属性值。\n\n所以本例中我们采用key: 秒杀商品ID值；value：秒杀商品数据（实体类）。\n\n```java\n    //设置秒杀redis缓存的key\n    private final String key = \"seckill\";\n\n    @Override\n    public List<Seckill> findAll() {\n        List<Seckill> seckillList = redisTemplate.boundHashOps(\"seckill\").values();\n        if (seckillList == null || seckillList.size() == 0){\n            //说明缓存中没有秒杀列表数据\n            //查询数据库中秒杀列表数据，并将列表数据循环放入redis缓存中\n            seckillList = seckillMapper.findAll();\n            for (Seckill seckill : seckillList){\n                //将秒杀列表数据依次放入redis缓存中，key:秒杀表的ID值；value:秒杀商品数据\n                redisTemplate.boundHashOps(key).put(seckill.getSeckillId(), seckill);\n                logger.info(\"findAll -> 从数据库中读取放入缓存中\");\n            }\n        }else{\n            logger.info(\"findAll -> 从缓存中读取\");\n        }\n        return seckillList;\n    }\n```\n\n## 优化exportSeckillUrl方法\n\n`exportSeckillUrl()`暴露接口的方法也是常调用的，因为其中要频繁的调用`findById()`方法，所以将指定ID的商品数据放入缓存中也是很必要的，当然我们在之前的findAll()方法中已经将每个ID的数据都分别放入了缓存中。\n\n```java\n    @Override\n    public Exposer exportSeckillUrl(long seckillId) {\n        Seckill seckill = (Seckill) redisTemplate.boundHashOps(key).get(seckillId);\n        if (seckill == null) {\n            //说明redis缓存中没有此key对应的value\n            //查询数据库，并将数据放入缓存中\n            seckill = seckillMapper.findById(seckillId);\n            if (seckill == null) {\n                //说明没有查询到\n                return new Exposer(false, seckillId);\n            } else {\n                //查询到了，存入redis缓存中。 key:秒杀表的ID值； value:秒杀表数据\n                redisTemplate.boundHashOps(key).put(seckill.getSeckillId(), seckill);\n                logger.info(\"RedisTemplate -> 从数据库中读取并放入缓存中\");\n            }\n        } else {\n            logger.info(\"RedisTemplate -> 从缓存中读取\");\n        }\n        Date startTime = seckill.getStartTime();\n        Date endTime = seckill.getEndTime();\n        //获取系统时间\n        Date nowTime = new Date();\n        if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {\n            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());\n        }\n        //转换特定字符串的过程，不可逆的算法\n        String md5 = getMD5(seckillId);\n        return new Exposer(true, md5, seckillId);\n    }\n```\n\n## 优化executeSeckill方法\n\n上面的两个查询操作都将商品数据放入了缓存中，key:商品ID；value:商品数据（实体类）。而对于减库存操作，用户每次抢购一件商品，商品的库存总量都需要-1，但是我们页面展示的数据都是从缓存中读取的，即使修改了数据库中的库存数量，页面上展示的数量仍是无法修改的，所以我们同时要修改缓存中的数据来保证缓存和数据库数据的一致性。\n\n更新缓存的办法就简单了，即重新`put`进去数据就行了，因为Redis缓存数据库中存放的数据是key-value形式，你重新对指定key put进去新的值，就势必会覆盖掉原来的值（这也就是我们为什么设计key:商品ID；value:商品数据）。\n\n```java\n@Override\n    @Transactional\n    public SeckillExecution executeSeckill(long seckillId, BigDecimal money, long userPhone, String md5)\n            throws SeckillException, RepeatKillException, SeckillCloseException {\n        if (md5 == null || !md5.equals(getMD5(seckillId))) {\n            throw new SeckillException(\"seckill data rewrite\");\n        }\n        //执行秒杀逻辑：1.减库存；2.储存秒杀订单\n        Date nowTime = new Date();\n\n        try {\n            //记录秒杀订单信息\n            int insertCount = seckillOrderMapper.insertOrder(seckillId, money, userPhone);\n            //唯一性：seckillId,userPhone，保证一个用户只能秒杀一件商品\n            if (insertCount <= 0) {\n                //重复秒杀\n                throw new RepeatKillException(\"seckill repeated\");\n            } else {\n                //减库存\n                int updateCount = seckillMapper.reduceStock(seckillId, nowTime);\n                if (updateCount <= 0) {\n                    //没有更新记录，秒杀结束\n                    throw new SeckillCloseException(\"seckill is closed\");\n                } else {\n                    //秒杀成功\n                    SeckillOrder seckillOrder = seckillOrderMapper.findById(seckillId);\n\n                    //更新缓存（更新库存数量）\n                    Seckill seckill = (Seckill) redisTemplate.boundHashOps(key).get(seckillId);\n                    seckill.setStockCount(seckill.getSeckillId() - 1);\n                    redisTemplate.boundHashOps(key).put(seckillId, seckill);\n\n                    return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, seckillOrder);\n                }\n            }\n        } catch (SeckillCloseException e) {\n            throw e;\n        } catch (RepeatKillException e) {\n            throw e;\n        } catch (Exception e) {\n            logger.error(e.getMessage(), e);\n            //所有编译期异常，转换为运行期异常\n            throw new SeckillException(\"seckill inner error:\" + e.getMessage());\n        }\n    }\n```\n\n\n<br/>\n\n# 交流\n\n如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！\n\n<br/>\n\n# 联系\n\nIf you have some questions after you see this article, you can contact me or you can find some info by clicking these links.\n\n- [Blog@TyCoding\'s blog](http://www.tycoding.cn)\n- [GitHub@TyCoding](https://github.com/TyCoding)\n- [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)\n', NULL, 'http://tycoding.cn', '1', 763, '2018-09-16 12:00:00', '2018-10-16 12:00:00', '2018-10-16 12:00:00', 0);
INSERT INTO `tb_article` VALUES (2, 'SpringBoot实现Java高并发秒杀系统之Web层开发（三）', '/site/images/thumbs/2.jpg', '涂陌', '<p>接着上一篇文章：<a href=\"http://tycoding.cn/2018/10/13/seckill-service/\">SpringBoot实现Java高并发之Service层开发</a>，今天我们开始讲SpringBoot实现Java高并发秒杀系统之Web层开发。</p>\n<p>Web层即Controller层，当然我们所说的都是在基于Spring框架的系统上而言的，传统的SSH项目中，与页面进行交互的是struts框架，但struts框架很繁琐，后来就被SpringMVC给顶替了，SpringMVC框架在与页面的交互上提供了更加便捷的方式，MVC的设计模式也是当前非常流行的一种设计模式。这次我们针对秒杀系统讲解一下秒杀系统需要和页面交互的操作和数据都涉及哪些？</p>\n<p>本项目的源码请参看：<a href=\"https://github.com/TyCoding/springboot-seckill\">springboot-seckill</a>  如果觉得不错可以star一下哦(#^.^#)</p>\n<!--more-->\n<p>本项目一共分为四个模块来讲解，具体的开发教程请看我的博客文章：</p>\n<ul>\n<li><p><a href=\"http://tycoding.cn/2018/10/12/seckill-dao/\">SpringBoot实现Java高并发秒杀系统之DAO层开发（一）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/13/seckill-service/\">SpringBoot实现Java高并发秒杀系统之Service层开发（二）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/14/seckill-web/\">SpringBoot实现Java高并发秒杀系统之Web层开发（三）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/15/seckill/\">SpringBoot实现Java高并发秒杀系统之并发优化（四）</a></p>\n</li></ul>\n<p>首先如果你对SpringBoot项目还是不清楚的话，我依然推荐你看一下我的这个项目：<a href=\"https://github.com/TyCoding/spring-boot\">优雅的入门SpringBoot2.x，整合Mybatis实现CRUD</a></p>\n<h1 id=\"h1-u524Du7AEFu4EA4u4E92u6D41u7A0Bu8BBEu8BA1\"><a name=\"前端交互流程设计\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>前端交互流程设计</h1><p>编写Controller就是要搞清楚：1.页面需要什么数据？2.页面将返回给Controller什么数据？3.Controller应该返回给页面什么数据？</p>\n<p>带着这些问题我们看一下秒杀详情页流程逻辑（不再讲基本的<code>findById</code>和<code>findAll()</code>方法）：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-b3f724602462c24b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>因为整个秒杀系统中最核心的业务就是：1.减库存；2.查询订单明细。我们看一下Controller层的源码：</p>\n<pre><code class=\"lang-java\">@Controller\n@RequestMapping(&quot;/seckill&quot;)\npublic class SeckillController {\n\n    @Autowired\n    private SeckillService seckillService;\n\n    private final Logger logger = LoggerFactory.getLogger(this.getClass());\n\n    @ResponseBody\n    @RequestMapping(&quot;/findAll&quot;)\n    public List&lt;Seckill&gt; findAll() {\n        return seckillService.findAll();\n    }\n\n    @ResponseBody\n    @RequestMapping(&quot;/findById&quot;)\n    public Seckill findById(@RequestParam(&quot;id&quot;) Long id) {\n        return seckillService.findById(id);\n    }\n\n    @RequestMapping(&quot;/{seckillId}/detail&quot;)\n    public String detail(@PathVariable(&quot;seckillId&quot;) Long seckillId, Model model) {\n        if (seckillId == null) {\n            return &quot;page/seckill&quot;;\n        }\n        Seckill seckill = seckillService.findById(seckillId);\n        model.addAttribute(&quot;seckill&quot;, seckill);\n        if (seckill == null) {\n            return &quot;page/seckill&quot;;\n        }\n        return &quot;page/seckill_detail&quot;;\n    }\n\n    @ResponseBody\n    @RequestMapping(value = &quot;/{seckillId}/exposer&quot;,\n            method = RequestMethod.POST, produces = {&quot;application/json;charset=UTF-8&quot;})\n    public SeckillResult&lt;Exposer&gt; exposer(@PathVariable(&quot;seckillId&quot;) Long seckillId) {\n        SeckillResult&lt;Exposer&gt; result;\n        try {\n            Exposer exposer = seckillService.exportSeckillUrl(seckillId);\n            result = new SeckillResult&lt;Exposer&gt;(true, exposer);\n        } catch (Exception e) {\n            logger.error(e.getMessage(), e);\n            result = new SeckillResult&lt;Exposer&gt;(false, e.getMessage());\n        }\n        return result;\n    }\n\n    @RequestMapping(value = &quot;/{seckillId}/{md5}/execution&quot;,\n            method = RequestMethod.POST,\n            produces = {&quot;application/json;charset=UTF-8&quot;})\n    @ResponseBody\n    public SeckillResult&lt;SeckillExecution&gt; execute(@PathVariable(&quot;seckillId&quot;) Long seckillId,\n                                                   @PathVariable(&quot;md5&quot;) String md5,\n                                                   @RequestParam(&quot;money&quot;) BigDecimal money,\n                                                   @CookieValue(value = &quot;killPhone&quot;, required = false) Long userPhone) {\n        if (userPhone == null) {\n            return new SeckillResult&lt;SeckillExecution&gt;(false, &quot;未注册&quot;);\n        }\n        try {\n            SeckillExecution execution = seckillService.executeSeckill(seckillId, money, userPhone, md5);\n            return new SeckillResult&lt;SeckillExecution&gt;(true, execution);\n        } catch (RepeatKillException e) {\n            SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);\n            return new SeckillResult&lt;SeckillExecution&gt;(true, seckillExecution);\n        } catch (SeckillCloseException e) {\n            SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.END);\n            return new SeckillResult&lt;SeckillExecution&gt;(true, seckillExecution);\n        } catch (SeckillException e) {\n            SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);\n            return new SeckillResult&lt;SeckillExecution&gt;(true, seckillExecution);\n        }\n    }\n\n    @ResponseBody\n    @GetMapping(value = &quot;/time/now&quot;)\n    public SeckillResult&lt;Long&gt; time() {\n        Date now = new Date();\n        return new SeckillResult(true, now.getTime());\n    }\n}\n</code></pre>\n<p>下面我以问答的形式讲解一下Controller层方法的定义：</p>\n<blockquote>\n<p>1.<code><a href=\"https://github.com/ResponseBody\" title=\"&#64;ResponseBody\" class=\"at-link\">@ResponseBody</a></code>和<code><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\">@RestController</a></code>注解分别有什么作用？</p>\n</blockquote>\n<ul>\n<li><p><code><a href=\"https://github.com/ResponseBody\" title=\"&#64;ResponseBody\" class=\"at-link\"><a href=\"https://github.com/ResponseBody\" title=\"&#64;ResponseBody\" class=\"at-link\">@ResponseBody</a></a></code>注解标识的方法，Spring会将此方法return的数据转换成JSON格式且不会被Spring视图解析器所扫描到，也就是此方法永不可能返回一个视图页面。且这个注解只能用在方法体上，不能用在类上。</p>\n</li><li><p><code><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\"><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\">@RestController</a></a></code>注解标识的类，Spring会将其下的所有方法return的数据都转换成JSON格式且不会被Spring视图解析器扫描到，也就是此类下面的所有方法都不可能返回一个视图页面。且这个注解只能用在类上，不能用在方法体上。</p>\n</li></ul>\n<blockquote>\n<p>2.<code><a href=\"https://github.com/RequestMapping\" title=\"&#64;RequestMapping\" class=\"at-link\">@RequestMapping</a></code>中<code>{xx}</code>的语法是什么？<code><a href=\"https://github.com/PathVariable\" title=\"&#64;PathVariable\" class=\"at-link\">@PathVariable</a></code>注解的用处是什么？</p>\n</blockquote>\n<p>Spring框架很早就支持开发REST资源。也是就是现在我们定义的RESTful URL，在Spring框架上支持的尤为完美，我们可以在Controller中定义这样一个URL映射地址：<code>/{id}/detail</code>，他是合理的RESTful URL定义方式。</p>\n<p>这种URL的特点：URL地址由动态的数据拼接组成的，而不是将所有的资源全部映射到一个路径下，比如：<code>/article/detail</code>。</p>\n<p>这种URL结构的优势：我们能很容易从URL地址上判断出该地址所展示的页面是什么？比如：<code>/1/detail</code>就可能表示ID为1的文章的详情页，看起来设计的很清晰。</p>\n<p>这种URL如何进行交互：我们定义了<code>/{id}/detail</code>这样一个URL映射地址，其对应的映射方法上就应该添加<code><a href=\"https://github.com/PathVariable\" title=\"&#64;PathVariable\" class=\"at-link\">@PathVariable</a></code>注解标识，如：<code><a href=\"https://github.com/PathVariable\" title=\"&#64;PathVariable\" class=\"at-link\">@PathVariable</a>(&quot;id&quot;) Long id</code>Spring就能装配前端传递的URL中指定位置的数据并赋值给<code>id</code>这个参数。比如前端调用后端接口：<code>localhost:8080/seckill/1/detail</code>，后端存在一个映射方法：<code><a href=\"https://github.com/RequestMapping\" title=\"&#64;RequestMapping\" class=\"at-link\">@RequestMapping</a>(&quot;/{id}/detail&quot;)</code>，这样就能刚好匹配上这个URL映射地址。</p>\n<p>所以我们看一下秒杀系统的RESTful URL设计：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-b6747e68ac46b933.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<blockquote>\n<p>3.为什么要单独写一个接口用来获取当前系统时间？</p>\n</blockquote>\n<p>由于我们开发的系统肯定不是给自己用的，我们的用户可能处于不同的时区，他们的当前系统时间也是不同的，所以我们写一个通用的时间规范：就是当前服务器的时间。</p>\n<blockquote>\n<p>4.SeckillResult是什么？</p>\n</blockquote>\n<p>在前面我们将Service层系统开发的时候就手动创建了很多类来封装一些通用的结果信息。而对于Controller层也会返回很多结果数据，比如传入的URL中id值为null，那么就没必要继续向下请求，而是直接给页面返回false信息。</p>\n<p>于是我们创建：<code>SeckillResult.java</code></p>\n<pre><code class=\"lang-java\">public class SeckillResult&lt;T&gt; {\n\n    private boolean success;\n\n    private T data;\n\n    private String error;\n\n    public SeckillResult(boolean success, T data) {\n        this.success = success;\n        this.data = data;\n    }\n\n    public SeckillResult(boolean success, String error) {\n        this.success = success;\n        this.error = error;\n    }\n}\n</code></pre>\n<p>泛型<code>T</code>表示可以代表不同类型的对象。这是泛型类应用很广泛的一个特性，我们调用SeckillResult类，将其中的T用什么替换那么T就表示这个替换的对象类型。</p>\n<h1 id=\"h1-u9875u9762u8BBEu8BA1\"><a name=\"页面设计\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>页面设计</h1><p><strong>用了哪些技术？</strong></p>\n<ol>\n<li>HTML页面，用Bootstrap绘制。</li><li>Thymeleaf模板引擎渲染HTML页面，使得HTML页面拥有类似JSP页面一样功能。</li><li>JS方面使用原生的JQuery。</li></ol>\n<p>我们来看一下前端的页面设计：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-bae981f77bcfb4ef.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>本项目使用Cookie存储用户手机号的方式模拟用户登录功能，实际上没有与后端交互的操作。如果用户没有登录就打开了商品详情页会直接弹出一个手机号登录框提醒用户登录，且没有登录时无法关闭登录框的。</p>\n<p>具体的源码请看：<a href=\"https://github.com/TyCoding/spring-boot/tree/master/src/main/resources\">GitHub</a></p>\n<h2 id=\"h2-u601Du8003\"><a name=\"思考\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>思考</h2><p>在从JSP页面转换到HTML页面的时候我常会遇到这么一个问题：前端如何取出来后端查询到的数据？</p>\n<p>在之前我们写的JSP页面中，可以通过将后端查询到的数据放进request,session域对象中，JSP页面可以直接调用Java中域对象的数据，甚至可以通过EL表达式（<code>${}</code>）来直接获取参数，但是这种方法有一个弊端：Controller必须是返回一个视图，这样才能在此视图中获取存进域对象中的数据。</p>\n<p>而我们现在都开始用HTML页面，也无法从域对象中取出数据该怎么办呢？我这里提供两个思路：</p>\n<ul>\n<li><p>1.像本项目中一样，前端使用Thymeleaf模板引擎渲染页面，那么Thymeleaf内置很多方法如同JSP页面的EL表达式。Thymeleaf在HTML中取出域对象数据使用：<code>&lt;span th:text=&quot;${xx}&quot;&gt;</code>；在JS中取出域对象数据：<code>var v = [[${xx}]]</code>（当然都必须是在HTML页面中，在外部JS文件中是得不到数据的）。</p>\n</li><li><p>2.使用原生js提供的<code>location</code>对象，我们先看一下URL的组成结构：</p>\n</li></ul>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-1a98b5f917e1e6f7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>详细介绍请看：<a href=\"https://blog.csdn.net/J_Y_X_8/article/details/51272098?locationNum=9&amp;fps=1\">博文</a></p>\n<p><strong>举个栗子</strong></p>\n<pre><code class=\"lang-javascript\">function QueryUrl(name){\n     var reg = new RegExp(&quot;(^|&amp;)&quot;+ name +&quot;=([^&amp;]*)(&amp;|$)&quot;);\n     var r = window.location.search.substr(1).match(reg);\n     if(r!=null)return  unescape(r[2]); return null;\n}\n\n// 调用方法\nalert(QueryUrl(&quot;参数名1&quot;));\n</code></pre>\n<p><br/></p>\n<h1 id=\"h1-u4EA4u6D41\"><a name=\"交流\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>交流</h1><p>如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！</p>\n<p><br/></p>\n<h1 id=\"h1-u8054u7CFB\"><a name=\"联系\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>联系</h1><p>If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.</p>\n<ul>\n<li><a href=\"http://www.tycoding.cn\"\">Blog&#64;TyCoding’s blog</a></li><li><a href=\"https://github.com/TyCoding\"\">GitHub&#64;TyCoding</a></li><li><a href=\"https://www.zhihu.com/people/tomo-83-82/activities\"\">ZhiHu&#64;TyCoding</a></li></ul>\n', '\n接着上一篇文章：[SpringBoot实现Java高并发之Service层开发](http://tycoding.cn/2018/10/13/seckill-service/)，今天我们开始讲SpringBoot实现Java高并发秒杀系统之Web层开发。\n\nWeb层即Controller层，当然我们所说的都是在基于Spring框架的系统上而言的，传统的SSH项目中，与页面进行交互的是struts框架，但struts框架很繁琐，后来就被SpringMVC给顶替了，SpringMVC框架在与页面的交互上提供了更加便捷的方式，MVC的设计模式也是当前非常流行的一种设计模式。这次我们针对秒杀系统讲解一下秒杀系统需要和页面交互的操作和数据都涉及哪些？\n\n本项目的源码请参看：[springboot-seckill](https://github.com/TyCoding/springboot-seckill)  如果觉得不错可以star一下哦(#^.^#)\n\n<!--more-->\n\n本项目一共分为四个模块来讲解，具体的开发教程请看我的博客文章：\n\n* [SpringBoot实现Java高并发秒杀系统之DAO层开发（一）](http://tycoding.cn/2018/10/12/seckill-dao/)\n\n* [SpringBoot实现Java高并发秒杀系统之Service层开发（二）](http://tycoding.cn/2018/10/13/seckill-service/)\n\n* [SpringBoot实现Java高并发秒杀系统之Web层开发（三）](http://tycoding.cn/2018/10/14/seckill-web/)\n\n* [SpringBoot实现Java高并发秒杀系统之并发优化（四）](http://tycoding.cn/2018/10/15/seckill/)\n\n首先如果你对SpringBoot项目还是不清楚的话，我依然推荐你看一下我的这个项目：[优雅的入门SpringBoot2.x，整合Mybatis实现CRUD](https://github.com/TyCoding/spring-boot)\n\n\n# 前端交互流程设计\n\n编写Controller就是要搞清楚：1.页面需要什么数据？2.页面将返回给Controller什么数据？3.Controller应该返回给页面什么数据？\n\n带着这些问题我们看一下秒杀详情页流程逻辑（不再讲基本的`findById`和`findAll()`方法）：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-b3f724602462c24b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n因为整个秒杀系统中最核心的业务就是：1.减库存；2.查询订单明细。我们看一下Controller层的源码：\n\n```java\n@Controller\n@RequestMapping(\"/seckill\")\npublic class SeckillController {\n\n    @Autowired\n    private SeckillService seckillService;\n\n    private final Logger logger = LoggerFactory.getLogger(this.getClass());\n    \n    @ResponseBody\n    @RequestMapping(\"/findAll\")\n    public List<Seckill> findAll() {\n        return seckillService.findAll();\n    }\n    \n    @ResponseBody\n    @RequestMapping(\"/findById\")\n    public Seckill findById(@RequestParam(\"id\") Long id) {\n        return seckillService.findById(id);\n    }\n    \n    @RequestMapping(\"/{seckillId}/detail\")\n    public String detail(@PathVariable(\"seckillId\") Long seckillId, Model model) {\n        if (seckillId == null) {\n            return \"page/seckill\";\n        }\n        Seckill seckill = seckillService.findById(seckillId);\n        model.addAttribute(\"seckill\", seckill);\n        if (seckill == null) {\n            return \"page/seckill\";\n        }\n        return \"page/seckill_detail\";\n    }\n\n    @ResponseBody\n    @RequestMapping(value = \"/{seckillId}/exposer\",\n            method = RequestMethod.POST, produces = {\"application/json;charset=UTF-8\"})\n    public SeckillResult<Exposer> exposer(@PathVariable(\"seckillId\") Long seckillId) {\n        SeckillResult<Exposer> result;\n        try {\n            Exposer exposer = seckillService.exportSeckillUrl(seckillId);\n            result = new SeckillResult<Exposer>(true, exposer);\n        } catch (Exception e) {\n            logger.error(e.getMessage(), e);\n            result = new SeckillResult<Exposer>(false, e.getMessage());\n        }\n        return result;\n    }\n\n    @RequestMapping(value = \"/{seckillId}/{md5}/execution\",\n            method = RequestMethod.POST,\n            produces = {\"application/json;charset=UTF-8\"})\n    @ResponseBody\n    public SeckillResult<SeckillExecution> execute(@PathVariable(\"seckillId\") Long seckillId,\n                                                   @PathVariable(\"md5\") String md5,\n                                                   @RequestParam(\"money\") BigDecimal money,\n                                                   @CookieValue(value = \"killPhone\", required = false) Long userPhone) {\n        if (userPhone == null) {\n            return new SeckillResult<SeckillExecution>(false, \"未注册\");\n        }\n        try {\n            SeckillExecution execution = seckillService.executeSeckill(seckillId, money, userPhone, md5);\n            return new SeckillResult<SeckillExecution>(true, execution);\n        } catch (RepeatKillException e) {\n            SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);\n            return new SeckillResult<SeckillExecution>(true, seckillExecution);\n        } catch (SeckillCloseException e) {\n            SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.END);\n            return new SeckillResult<SeckillExecution>(true, seckillExecution);\n        } catch (SeckillException e) {\n            SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);\n            return new SeckillResult<SeckillExecution>(true, seckillExecution);\n        }\n    }\n\n    @ResponseBody\n    @GetMapping(value = \"/time/now\")\n    public SeckillResult<Long> time() {\n        Date now = new Date();\n        return new SeckillResult(true, now.getTime());\n    }\n}\n```\n\n下面我以问答的形式讲解一下Controller层方法的定义：\n\n> 1.`@ResponseBody`和`@RestController`注解分别有什么作用？\n\n* `@ResponseBody`注解标识的方法，Spring会将此方法return的数据转换成JSON格式且不会被Spring视图解析器所扫描到，也就是此方法永不可能返回一个视图页面。且这个注解只能用在方法体上，不能用在类上。\n\n* `@RestController`注解标识的类，Spring会将其下的所有方法return的数据都转换成JSON格式且不会被Spring视图解析器扫描到，也就是此类下面的所有方法都不可能返回一个视图页面。且这个注解只能用在类上，不能用在方法体上。\n\n> 2.`@RequestMapping`中`{xx}`的语法是什么？`@PathVariable`注解的用处是什么？\n\nSpring框架很早就支持开发REST资源。也是就是现在我们定义的RESTful URL，在Spring框架上支持的尤为完美，我们可以在Controller中定义这样一个URL映射地址：`/{id}/detail`，他是合理的RESTful URL定义方式。\n\n这种URL的特点：URL地址由动态的数据拼接组成的，而不是将所有的资源全部映射到一个路径下，比如：`/article/detail`。\n\n这种URL结构的优势：我们能很容易从URL地址上判断出该地址所展示的页面是什么？比如：`/1/detail`就可能表示ID为1的文章的详情页，看起来设计的很清晰。\n\n这种URL如何进行交互：我们定义了`/{id}/detail`这样一个URL映射地址，其对应的映射方法上就应该添加`@PathVariable`注解标识，如：`@PathVariable(\"id\") Long id`Spring就能装配前端传递的URL中指定位置的数据并赋值给`id`这个参数。比如前端调用后端接口：`localhost:8080/seckill/1/detail`，后端存在一个映射方法：`@RequestMapping(\"/{id}/detail\")`，这样就能刚好匹配上这个URL映射地址。\n\n所以我们看一下秒杀系统的RESTful URL设计：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-b6747e68ac46b933.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n> 3.为什么要单独写一个接口用来获取当前系统时间？\n\n由于我们开发的系统肯定不是给自己用的，我们的用户可能处于不同的时区，他们的当前系统时间也是不同的，所以我们写一个通用的时间规范：就是当前服务器的时间。\n\n> 4.SeckillResult是什么？\n\n在前面我们将Service层系统开发的时候就手动创建了很多类来封装一些通用的结果信息。而对于Controller层也会返回很多结果数据，比如传入的URL中id值为null，那么就没必要继续向下请求，而是直接给页面返回false信息。\n\n于是我们创建：`SeckillResult.java`\n\n```java\npublic class SeckillResult<T> {\n\n    private boolean success;\n\n    private T data;\n\n    private String error;\n\n    public SeckillResult(boolean success, T data) {\n        this.success = success;\n        this.data = data;\n    }\n\n    public SeckillResult(boolean success, String error) {\n        this.success = success;\n        this.error = error;\n    }\n}\n```\n\n泛型`T`表示可以代表不同类型的对象。这是泛型类应用很广泛的一个特性，我们调用SeckillResult类，将其中的T用什么替换那么T就表示这个替换的对象类型。\n\n\n\n\n# 页面设计\n\n**用了哪些技术？**\n\n1. HTML页面，用Bootstrap绘制。\n2. Thymeleaf模板引擎渲染HTML页面，使得HTML页面拥有类似JSP页面一样功能。\n3. JS方面使用原生的JQuery。\n\n我们来看一下前端的页面设计：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-bae981f77bcfb4ef.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n本项目使用Cookie存储用户手机号的方式模拟用户登录功能，实际上没有与后端交互的操作。如果用户没有登录就打开了商品详情页会直接弹出一个手机号登录框提醒用户登录，且没有登录时无法关闭登录框的。\n\n具体的源码请看：[GitHub](https://github.com/TyCoding/spring-boot/tree/master/src/main/resources)\n\n## 思考\n\n在从JSP页面转换到HTML页面的时候我常会遇到这么一个问题：前端如何取出来后端查询到的数据？\n\n在之前我们写的JSP页面中，可以通过将后端查询到的数据放进request,session域对象中，JSP页面可以直接调用Java中域对象的数据，甚至可以通过EL表达式（`${}`）来直接获取参数，但是这种方法有一个弊端：Controller必须是返回一个视图，这样才能在此视图中获取存进域对象中的数据。\n\n而我们现在都开始用HTML页面，也无法从域对象中取出数据该怎么办呢？我这里提供两个思路：\n\n* 1.像本项目中一样，前端使用Thymeleaf模板引擎渲染页面，那么Thymeleaf内置很多方法如同JSP页面的EL表达式。Thymeleaf在HTML中取出域对象数据使用：`<span th:text=\"${xx}\">`；在JS中取出域对象数据：`var v = [[${xx}]]`（当然都必须是在HTML页面中，在外部JS文件中是得不到数据的）。\n\n* 2.使用原生js提供的`location`对象，我们先看一下URL的组成结构：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-1a98b5f917e1e6f7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n详细介绍请看：[博文](https://blog.csdn.net/J_Y_X_8/article/details/51272098?locationNum=9&fps=1)\n\n**举个栗子**\n\n```javascript\nfunction QueryUrl(name){\n     var reg = new RegExp(\"(^|&)\"+ name +\"=([^&]*)(&|$)\");\n     var r = window.location.search.substr(1).match(reg);\n     if(r!=null)return  unescape(r[2]); return null;\n}\n\n// 调用方法\nalert(QueryUrl(\"参数名1\"));\n```\n\n<br/>\n\n# 交流\n\n如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！\n\n<br/>\n\n# 联系\n\nIf you have some questions after you see this article, you can contact me or you can find some info by clicking these links.\n\n- [Blog@TyCoding\'s blog](http://www.tycoding.cn)\n- [GitHub@TyCoding](https://github.com/TyCoding)\n- [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)\n', NULL, 'http://tycoding.cn', '1', 34813, '2018-09-17 10:39:28', '2018-10-17 10:39:28', '2018-10-17 10:39:28', 0);
INSERT INTO `tb_article` VALUES (4, 'SpringBoot实现Java高并发秒杀系统之DAO层开发（一）', '/site/images/thumbs/12.jpg', '涂陌', '<p><strong>秒杀</strong>系统在如今电商项目中是很常见的，最近在学习电商项目时讲到了秒杀系统的实现，于是打算使用SpringBoot框架学习一下秒杀系统（本项目基于慕课网的一套免费视频教程：<a href=\"https://www.imooc.com/u/2145618/courses?sort=publish\">Java高并发秒杀API</a>，视频教程中讲解的很详细，非常感谢这位讲师）。也是因为最近学习了SpringBoot框架（GitHub教程：<a href=\"https://github.com/TyCoding/spring-boot\">SpringBoot入门之CRUD</a> ），觉得SpringBoot框架确实比传统SSM框架方便了很多，于是更深层次练习使用SpringBoot框架，注意：<strong>SpringBoot不是对Spring功能上的增强，而是提供了一种快速使用Spring的方式。</strong> 如果你熟悉了SSM框架，学习SpringBoot框架也是很Easy的。</p>\n<p>本项目的源码请参看：<a href=\"https://github.com/TyCoding/springboot-seckill\">springboot-seckill</a>  如果觉得不错可以star一下哦(#^.^#)</p>\n<!--more-->\n<p>本项目一共分为四个模块来讲解，具体的开发教程请看我的博客文章：</p>\n<ul>\n<li><p><a href=\"http://tycoding.cn/2018/10/12/seckill-dao/\">SpringBoot实现Java高并发秒杀系统之DAO层开发（一）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/13/seckill-service/\">SpringBoot实现Java高并发秒杀系统之Service层开发（二）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/14/seckill-web/\">SpringBoot实现Java高并发秒杀系统之Web层开发（三）</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/10/15/seckill/\">SpringBoot实现Java高并发秒杀系统之并发优化（四）</a></p>\n</li></ul>\n<h1 id=\"h1-u8D77u6B65\"><a name=\"起步\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>起步</h1><p>首先我们需要搭建SpringBoot项目开发环境，IDEA搭建SpringBoot项目的具体教程请看我的：<a href=\"http://tycoding.cn/2018/09/30/springboot-mybatis/#more\">博文</a>。</p>\n<p>如果你对SpringBoot框架或是SSM框架不熟悉，我想推荐一下我的几个小项目帮助你更好的理解：</p>\n<ul>\n<li><p><a href=\"http://tycoding.cn/2018/09/28/spring-boot/\">SpringBoot起步之环境搭建</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/09/30/springboot-mybatis/#more\">SpringBoot-Mybatis入门之CRUD</a></p>\n</li><li><p><a href=\"https://github.com/TyCoding/ssm\">手把手教你整合SSM框架</a></p>\n</li><li><p><a href=\"http://tycoding.cn/2018/06/04/ssm/\">SSM框架入门之环境搭建</a></p>\n</li></ul>\n<p><br/></p>\n<p><strong>项目设计</strong></p>\n<pre><code>.\n├── README  -- Doc文档\n├── db  -- 数据库约束文件\n├── mvnw\n├── mvnw.cmd\n├── pom.xml  -- 项目依赖\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── cn\n    │   │       └── tycoding\n    │   │           ├── SpringbootSeckillApplication.java  -- SpringBoot启动器\n    │   │           ├── controller  -- MVC的web层\n    │   │           ├── dto  -- 统一封装的一些结果属性，和entity类似\n    │   │           ├── entity  -- 实体类\n    │   │           ├── enums  -- 手动定义的字典枚举参数\n    │   │           ├── exception  -- 统一的异常结果\n    │   │           ├── mapper  -- Mybatis-Mapper层映射接口，或称为DAO层\n    │   │           ├── redis  -- redis,jedis 相关配置\n    │   │           └── service  -- 业务层\n    │   └── resources\n    │       ├── application.yml  -- SpringBoot核心配置\n    │       ├── mapper  -- Mybatis-Mapper层XML映射文件\n    │       ├── static  -- 存放页面静态资源，可通过浏览器直接访问\n    │       │   ├── css\n    │       │   ├── js\n    │       │   └── lib\n    │       └── templates  -- 存放Thymeleaf模板引擎所需的HTML，不能在浏览器直接访问\n    │           ├── page\n    │           └── public  -- HTML页面公共组件（头部、尾部）\n    └── test  -- 测试文件\n</code></pre><h2 id=\"h2-springboot\"><a name=\"SpringBoot\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>SpringBoot</h2><p>之前我们在<a href=\"https://github.com/TyCoding/spring-boot\">SpringBoot-Mybatis入门之CRUD</a>中已经详细讲解了SpringBoot框架的开发流程，还是觉得一句话说的特别好：<strong>SpringBoot不是对对Spring功能上的增强，而是提供了一种快速使用Spring的方式</strong>。所以用SSM阶段的知识足够了SpringBoot阶段的开发，下面我们强调一下小技巧：</p>\n<ul>\n<li><p>SpringBoot不需要配置注解扫描，之前我们配置<code>&lt;context:component-scan&gt;</code>扫描可能使用注解(<a href=\"https://github.com/Service\" title=\"&#64;Service\" class=\"at-link\"><a href=\"https://github.com/Service\" title=\"&#64;Service\" class=\"at-link\">@Service</a></a>,<a href=\"https://github.com/Component\" title=\"&#64;Component\" class=\"at-link\"><a href=\"https://github.com/Component\" title=\"&#64;Component\" class=\"at-link\">@Component</a></a>,<a href=\"https://github.com/Controller\" title=\"&#64;Controller\" class=\"at-link\"><a href=\"https://github.com/Controller\" title=\"&#64;Controller\" class=\"at-link\">@Controller</a></a>等)的包路径。默认创建SpringBoot项目自动生成的Application.java启动器类会自动扫描其下的所有注解。</p>\n</li><li><p>SpringBoot项目中静态资源都放在<code>resources</code>目录下，其中<code>static</code>目录中的数据可以直接通过浏览器访问，多用来放CSS、JS、img，但是不用来放html页面；其中<code>templates</code>用来存放HTML页面，但是需要在SpringBoot的配置文件(application.yml)中配置<code>spring.thymeleaf.prefix</code>标识Thymeleaf模板引擎渲染的页面位置。</p>\n</li><li><p>HTML页面通过Thymeleaf的加持，为HTML页面赋予了很多功能，此时的HTML页面类似于JSP页面。访问后端存入域对象(session,request…)中的数据，可以通过<code>th:text=&quot;${key}&quot;</code>获得，在JS中也可以通过<code>[[${key}]]</code>获得。</p>\n</li><li><p>Thymeleaf提供了类似JSP页面<code>&lt;include&gt;</code>的功能：public-component:<code>&lt;div th:fragment=&quot;header&quot;&gt;</code>，main-component:<code>&lt;div th:replace=&quot;path/header :: header&quot;&gt;</code>(其中<code>path</code>表示public-component相对于templates的路径，<code>/header</code>表示component文件名，最后的<code>header</code>表示<code>th:fragment</code>中定义的名称)。</p>\n</li></ul>\n<h2 id=\"h2-pom-\"><a name=\"pom依赖\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>pom依赖</h2><pre><code class=\"lang-xml\">    &lt;parent&gt;\n        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n        &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;\n        &lt;version&gt;2.0.5.RELEASE&lt;/version&gt;\n        &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;\n    &lt;/parent&gt;\n\n    &lt;properties&gt;\n        &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;\n        &lt;project.reporting.outputEncoding&gt;UTF-8&lt;/project.reporting.outputEncoding&gt;\n        &lt;java.version&gt;1.8&lt;/java.version&gt;\n    &lt;/properties&gt;\n\n    &lt;dependencies&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-thymeleaf&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.mybatis.spring.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;mybatis-spring-boot-starter&lt;/artifactId&gt;\n            &lt;version&gt;1.3.2&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-devtools&lt;/artifactId&gt;\n            &lt;scope&gt;runtime&lt;/scope&gt;\n        &lt;/dependency&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;mysql&lt;/groupId&gt;\n            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;\n            &lt;scope&gt;runtime&lt;/scope&gt;\n        &lt;/dependency&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;\n            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;\n            &lt;scope&gt;test&lt;/scope&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- alibaba的druid数据库连接池 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;com.alibaba&lt;/groupId&gt;\n            &lt;artifactId&gt;druid-spring-boot-starter&lt;/artifactId&gt;\n            &lt;version&gt;1.1.9&lt;/version&gt;\n        &lt;/dependency&gt;\n\n        &lt;!-- redis客户端 --&gt;\n        &lt;dependency&gt;\n            &lt;groupId&gt;redis.clients&lt;/groupId&gt;\n            &lt;artifactId&gt;jedis&lt;/artifactId&gt;\n        &lt;/dependency&gt;\n\n        &lt;dependency&gt;\n            &lt;groupId&gt;junit&lt;/groupId&gt;\n            &lt;artifactId&gt;junit&lt;/artifactId&gt;\n            &lt;version&gt;4.12&lt;/version&gt;\n            &lt;scope&gt;test&lt;/scope&gt;\n        &lt;/dependency&gt;\n    &lt;/dependencies&gt;\n</code></pre>\n<h2 id=\"h2-javabean-\"><a name=\"JavaBean实体类配置\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>JavaBean实体类配置</h2><p>此处源码请看：<a href=\"https://github.com/TyCoding/springboot-seckill/tree/master/src/main/java/cn/tycoding/entity\">GitHub</a></p>\n<p>Seckill.java</p>\n<pre><code class=\"lang-java\">public class Seckill implements Serializable {\n\n    private long seckillId; //商品ID\n    private String title; //商品标题\n    private String image; //商品图片\n    private BigDecimal price; //商品原价格\n    private BigDecimal costPrice; //商品秒杀价格\n\n    @DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)\n    @JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;, timezone = &quot;GMT+8&quot;)\n    private Date createTime; //创建时间\n\n    @DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)\n    @JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;, timezone = &quot;GMT+8&quot;)\n    private Date startTime; //秒杀开始时间\n\n    @DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)\n    @JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;, timezone = &quot;GMT+8&quot;)\n    private Date endTime; //秒杀结束时间\n\n    private long stockCount; //剩余库存数量\n}\n</code></pre>\n<p>SeckillOrder.java</p>\n<pre><code class=\"lang-java\">public class SeckillOrder implements Serializable {\n\n    private long seckillId; //秒杀到的商品ID\n    private BigDecimal money; //支付金额\n\n    private long userPhone; //秒杀用户的手机号\n\n    @DateTimeFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;)\n    @JsonFormat(pattern = &quot;yyyy-MM-dd HH:mm:ss&quot;, timezone = &quot;GMT+8&quot;)\n    private Date createTime; //创建时间\n\n    private boolean status; //订单状态， -1:无效 0:成功 1:已付款\n\n    private Seckill seckill; //秒杀商品，和订单是一对多的关系\n}\n</code></pre>\n<p>注意实体类中<code>Date</code>类型数据都用了<code><a href=\"https://github.com/DateTimeFormat\" title=\"&#64;DateTimeFormat\" class=\"at-link\">@DateTimeFormat</a>()</code>(来自springframework)和<code><a href=\"https://github.com/JsonFormat\" title=\"&#64;JsonFormat\" class=\"at-link\">@JsonFormat</a>()</code>(来自jackson)标识可以实现Controller在返回JSON数据（用<code><a href=\"https://github.com/ResponseBody\" title=\"&#64;ResponseBody\" class=\"at-link\">@ResponseBody</a></code>标识的方法或<code><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\">@RestController</a></code>标识的类）的时候能将Date类型的参数值（经Mybatis查询得到的数据是英文格式的日期，因为实体类中是Date类型）转换为注解中指定的格式返回给页面（相当于经过了一层SimpleDateFormate）。</p>\n<p>其次要<strong>注意</strong>在编写实体类的时候尽量养成习惯<strong>继承Serializable接口</strong>。在<code>SeckillOrder</code>中我们注入了<code>Seckill</code>类作为一个属性，目的是为了可以使用多表查询的方式从<code>seckill_order</code>表中查询出来对应的<code>seckill</code>表数据。</p>\n<h1 id=\"h1-u8868u8BBEu8BA1\"><a name=\"表设计\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>表设计</h1><p>创建完成了SpringBoot项目，首先我们需要初始化数据库，秒杀系统的建表SQL如下：</p>\n<pre><code class=\"lang-sql\">/*\n *  mysql-v: 5.7.22\n */\n\n-- 创建数据库\n-- CREATE DATABASE seckill DEFAULT CHARACTER SET utf8;\n\nDROP TABLE IF EXISTS `seckill`;\nDROP TABLE IF EXISTS `seckill_order`;\n\n-- 创建秒杀商品表\nCREATE TABLE `seckill`(\n  `seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT &#39;商品ID&#39;,\n  `title` varchar (1000) DEFAULT NULL COMMENT &#39;商品标题&#39;,\n  `image` varchar (1000) DEFAULT NULL COMMENT &#39;商品图片&#39;,\n  `price` decimal (10,2) DEFAULT NULL COMMENT &#39;商品原价格&#39;,\n  `cost_price` decimal (10,2) DEFAULT NULL COMMENT &#39;商品秒杀价格&#39;,\n  `stock_count` bigint DEFAULT NULL COMMENT &#39;剩余库存数量&#39;,\n  `start_time` timestamp NOT NULL DEFAULT &#39;1970-02-01 00:00:01&#39; COMMENT &#39;秒杀开始时间&#39;,\n  `end_time` timestamp NOT NULL DEFAULT &#39;1970-02-01 00:00:01&#39; COMMENT &#39;秒杀结束时间&#39;,\n  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT &#39;创建时间&#39;,\n  PRIMARY KEY (`seckill_id`),\n  KEY `idx_start_time` (`start_time`),\n  KEY `idx_end_time` (`end_time`),\n  KEY `idx_create_time` (`end_time`)\n) CHARSET=utf8 ENGINE=InnoDB COMMENT &#39;秒杀商品表&#39;;\n\n-- 创建秒杀订单表\nCREATE TABLE `seckill_order`(\n  `seckill_id` bigint NOT NULL COMMENT &#39;秒杀商品ID&#39;,\n  `money` decimal (10, 2) DEFAULT NULL COMMENT &#39;支付金额&#39;,\n  `user_phone` bigint NOT NULL COMMENT &#39;用户手机号&#39;,\n  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT &#39;创建时间&#39;,\n  `state` tinyint NOT NULL DEFAULT -1 COMMENT &#39;状态：-1无效 0成功 1已付款&#39;,\n  PRIMARY KEY (`seckill_id`, `user_phone`) /*联合主键，保证一个用户只能秒杀一件商品*/\n) CHARSET=utf8 ENGINE=InnoDB COMMENT &#39;秒杀订单表&#39;;\n</code></pre>\n<p><strong>解释</strong></p>\n<p>秒杀系统的表设计还是相对简单清晰的，这里我们只考虑秒杀系统的业务表，不涉及其他的表，所以整个系统主要涉及两张表：秒杀商品表、订单表。当然实际情况肯定不止这两张表（比如付款相关表，但是我们并未实现这个功能），也不止表中的这些字段。这里我们需要特别注意以下几点：</p>\n<p><strong>注意</strong></p>\n<ul>\n<li><p>1.我这里使用的Mysql版本是5.7.22，在Mysql5.7之后timestamp默认值不能再是<code>0000 00-00 00&#58;00&#58;00</code>，具体的介绍请看：<a href=\"https://dev.mysql.com/doc/refman/5.7/en/datetime.html\">mysql官方文档</a>。即 TIMESTAMP has a range of ‘1970-01-01 00&#58;00&#58;01’ UTC to ‘2038-01-19 03&#58;14&#58;07’ UTC.</p>\n</li><li><p>2.timestamp类型用来实现自动为新增行字段设置当前系统时间；且使用timestamp的字段必须给timestamp设置默认值，而在Mysql中date, datetime等类型都是无法实现默认设置当前系统时间值的功能(<code>DEFAULT CURRENT_TIMESTAMP</code>)的，所以我们必须使用timestamp类型，否则你要给字段传进来系统时间。</p>\n</li><li><p>3.decimal类型用于在数据库中设置精确的数值，比如<code>decimal(10,2)</code>表示可以存储10位且有2位小数的数值。</p>\n</li><li><p>4.tinyint类型用于存放int类型的数值，但是若用Mybatis作为DAO层框架，Mybatis会自动为tinyint类型的数据转换成true或false（0:false; 1 or 1+:true）。</p>\n</li><li><p>5.在订单表<code>seckill_order</code>中我们设计了联合主键：<code>PRIMARY KEY (seckill_id, user_phone)</code>，目的是为了避免单个用户重复购买同一件商品（一个用户只能秒杀到一次同一件商品）。</p>\n</li><li><p>6.无论是创建数据库还是创建表我们都应该养成一个习惯就是指定<code>character=utf-8</code>，避免中文数据乱码；其次还应该指定表的储存引擎是InnoDB，MySQL提供了两种储存引擎：InnoDB, MyISAM。但是只有InnoDB是支持事务的，且InnoDB相比MyISAM在并发上更具有高性能的优点。</p>\n</li></ul>\n<h1 id=\"h1-dao-\"><a name=\"DAO层开发\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>DAO层开发</h1><p>DAO层是我们常说的三层架构（Web层-业务层-持久层）中与数据库交互的持久层，但是实际而言，架构是这样设计的，但是并不代表着实际项目中就一定存在一个<code>dao</code>文件夹，特别是现阶段我们使用的Spring-Mybatis框架。Mybatis提供了一种接口代理开发模式，也就是我们需要提供一个interface接口，其他和数据库交互的SQL编写放到对应的XML文件中（但是需要进行相关的数据库参数配置，并且Mybatis规定了使用这种开发模式必须保持接口和XML文件名称对应）。于是在本项目中就没有出现<code>dao</code>整个文件夹，取而代之的是<code>mapper</code>这个文件夹，我感觉更易识别出为Mybatis的映射接口文件。其实在实际项目中考虑到项目的大小和复杂程度，<code>dao</code>和<code>mapper</code>可能是同时存在的，因为service可能并不满足项目的设计，即为dao接口创建实现类，在实现类中再调用mapper接口来实现功能模块的扩展。</p>\n<p><br/></p>\n<p>DAO层开发，即DAO层接口开发，主要设计需要和数据库交互的数据有哪些？应该用什么返回值类型接收查询到的数据？所以包含的方法有哪些？带着这些问题，我们先看一下秒杀系统的业务流程：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-0bf43f030ee56ae9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>由上图可以看出，相对与本项目而言和数据库打交道的主要涉及两个操作：1.减库存（秒杀商品表）；2.记录购买明细（订单表）。</p>\n<ul>\n<li><p>减库存，顾名思义就是减少当前被秒杀到的商品的库存数量，这也是秒杀系统中一个处理难点的地方。实现减库存即count-1，但是我们需要考虑Mysql的事务特性引发的种种问题、需要考虑如何避免同一用户重复秒杀的行为。</p>\n</li><li><p>如果减库存的业务解决了那么记录购买明细的业务就相对简单很多了，我们需要记录购买用户的姓名、手机号、购买的商品ID等。因为本项目中不涉及支付功能，所以记录用户的购买订单的业务并不复杂。</p>\n</li></ul>\n<p>分析了上面的功能，下面我们开始DAO层接口的编写(源码请看：<a href=\"https://github.com/TyCoding/springboot-seckill/tree/master/src/main/java/cn/tycoding/mapper\">GitHub</a>)：</p>\n<pre><code class=\"lang-java\">    /**\n     * 减库存。\n     * 对于Mapper映射接口方法中存在多个参数的要加@Param()注解标识字段名称，不然Mybatis不能识别出来哪个字段相互对应\n     *\n     * @param seckillId 秒杀商品ID\n     * @param killTime  秒杀时间\n     * @return 返回此SQL更新的记录数，如果&gt;=1表示更新成功\n     */\n    int reduceStock(@Param(&quot;seckillId&quot;) long seckillId, @Param(&quot;killTime&quot;) Date killTime);\n\n    /**\n     * 插入购买订单明细\n     *\n     * @param seckillId 秒杀到的商品ID\n     * @param money     秒杀的金额\n     * @param userPhone 秒杀的用户\n     * @return 返回该SQL更新的记录数，如果&gt;=1则更新成功\n     */\n    int insertOrder(@Param(&quot;seckillId&quot;) long seckillId, @Param(&quot;money&quot;) BigDecimal money, @Param(&quot;userPhone&quot;) long userPhone);\n</code></pre>\n<p>但从接口设计上我们无非关注的就是这两个方法：1.减库存；2.插入购买明细。此处需要注意的是：</p>\n<ul>\n<li><p>对于SpringBoot系统，DAO（Mapper）层的接口需要使用<code><a href=\"https://github.com/Mapper\" title=\"&#64;Mapper\" class=\"at-link\"><a href=\"https://github.com/Mapper\" title=\"&#64;Mapper\" class=\"at-link\">@Mapper</a></a></code>注解标识。因为SpringBoot系统中接口的XML文件不在<code>/java</code>目录下而是在<code>/resources</code>目录下。</p>\n</li><li><p>对于Mapper接口方法中存在传递多个参数的情况需要使用<code><a href=\"https://github.com/Param\" title=\"&#64;Param\" class=\"at-link\"><a href=\"https://github.com/Param\" title=\"&#64;Param\" class=\"at-link\">@Param</a></a>()</code>标识这个参数的名称，目的是为了帮助Mybatis识别传递的参数，不然Mybatis的XML中用的<code>#{}</code>不能识别出来你传递的参数名称是谁和谁对应的，类似于Controller层中常用的<code><a href=\"https://github.com/RequestParam\" title=\"&#64;RequestParam\" class=\"at-link\"><a href=\"https://github.com/RequestParam\" title=\"&#64;RequestParam\" class=\"at-link\">@RequestParam</a></a>()</code>注解。</p>\n</li><li><p><strong>小技巧:</strong> 之前我们做insert和update操作时直接用<code>void</code>作为方法返回值，实际上虽然Mybatis的<code>&lt;update&gt;</code>和<code>&lt;select&gt;</code>语句并没有<code>resultType</code>属性，但是并不代表其没有返回值，默认返回0或1，表示执行该SQL影响的行数。为此我们可以这样写SQL，如：<code>insert ignore into xxx</code>用来避免Mybatis报错，而是直接返回0表示当前SQL执行失败。</p>\n</li><li><p>小技巧：因为我们必须要避免同一个用户多次抢购同一件商品，在SQL中必须限制这一点（因为即使前端怎么控制都无法避免用户多次请求同一个接口，所谓接口防刷）。所以在设计订单表的时候用了联合主键且不自增的方式，以用户ID和用户电话组成联合主键，这样当同一个用户（电话相同）多次抢购同一件商品时插入的SQL就会产生主键冲突的问题，这样就会报错。</p>\n</li></ul>\n<h2 id=\"h2-xml-\"><a name=\"XML映射\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>XML映射</h2><pre><code class=\"lang-xml\">    &lt;update id=&quot;reduceStock&quot;&gt;\n        UPDATE seckill\n        SET stock_count = stock_count - 1\n        WHERE seckill_id = #{seckillId}\n        AND start_time &amp;lt;= #{killTime}\n        AND end_time &amp;gt;= #{killTime}\n        AND stock_count &amp;gt; 0\n    &lt;/update&gt;\n\n    &lt;insert id=&quot;insertOrder&quot;&gt;\n        INSERT ignore INTO seckill_order(seckill_id, money, user_phone)\n        VALUES (#{seckillId}, #{money}, #{userPhone})\n    &lt;/insert&gt;\n</code></pre>\n<p>SQL语句相对不是很复杂。减库存：执行update语句，令<code>stock_count</code>字段依次减一，并且当前要在一系列where条件的限制下；新增订单信息：保存订单数据，这里为接口防刷用联合主键<code>seckillId, userPhone</code>，如果同一个用户多次抢购同一件商品导致主键冲突会直接报错，为了避免系统不直接报错设计了<code>ignore</code>实现主键冲突就直接返回0表示该条SQL执行失败。</p>\n<p><strong>拓展</strong></p>\n<p>上面我使用了<code>&amp;lt;</code>、<code>&amp;gt;</code>的语法其实代表的是&gt;= &lt;=这种符号，因为在Mybatis中编写的SQL语句如果直接使用<code>&gt;=</code>或<code>&lt;=</code>这种判断条件可能会报错，我这里提供一种简单的解决方案就是用这种英文符号代替：</p>\n<table>\n<thead>\n<tr>\n<th>原符号</th>\n<th>替换符号</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>&lt;</td>\n<td>&lt;</td>\n</tr>\n<tr>\n<td>&lt;=</td>\n<td>&lt;=</td>\n</tr>\n<tr>\n<td>&gt;</td>\n<td>&gt;</td>\n</tr>\n<tr>\n<td>&gt;=</td>\n<td>&gt;=</td>\n</tr>\n<tr>\n<td>&amp;</td>\n<td>&amp;</td>\n</tr>\n<tr>\n<td>‘</td>\n<td>&apos;</td>\n</tr>\n<tr>\n<td>“</td>\n<td>&quot;</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"h3-order-findbyid-\"><a name=\"order表中findById方法\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>order表中findById方法</h3><p>之前在<code>SeckillOrder.java</code>实体类中我们注入了<code>Seckill</code>属性，用于可以根据查询<code>seckill_order</code>表的同时查询到其对应的<code>seckill</code>表数据，对应的接口定义如下：</p>\n<pre><code class=\"lang-java\">    /**\n     * 根据秒杀商品ID查询订单明细数据并得到对应秒杀商品的数据，因为我们再SeckillOrder中已经定义了一个Seckill的属性\n     *\n     * @param seckillId\n     * @return\n     */\n    SeckillOrder findById(long seckillId);\n</code></pre>\n<p>对应的SQL如下：</p>\n<pre><code class=\"lang-xml\">    &lt;select id=&quot;findById&quot; resultType=&quot;SeckillOrder&quot;&gt;\n        SELECT\n          so.seckill_id,\n          so.user_phone,\n          so.money,\n          so.create_time,\n          so.state,\n          s.seckill_id &quot;seckill.seckill_id&quot;,\n          s.title &quot;seckill.title&quot;,\n          s.cost_price &quot;seckill.cost_price&quot;,\n          s.create_time &quot;seckill.create_time&quot;,\n          s.start_time &quot;seckill.start_time&quot;,\n          s.end_time &quot;seckill.end_time&quot;,\n          s.stock_count &quot;seckill.stock_count&quot;\n        FROM seckill_order so\n        INNER JOIN seckill s ON so.seckill_id = s.seckill_id\n        WHERE so.seckill_id = #{seckillId}\n    &lt;/select&gt;\n</code></pre>\n<p>这个SQL看似复杂些，但是就是仅仅的多表（两张表）查询语句：根据<code>seckill_order</code>表中的<code>seckill_id</code>字段查询<code>seckill</code>表中<code>seckill_id</code>字段值对应的数据（也就是说：对于多表查询，其实两张表之间必然存在一定的字段关联关系，不一定是外键关联，当然我们也不建议用外键关联两张表）。</p>\n<p>其中<code>findById</code>的SQL中类似<code>s.seckill_id &quot;seckill.seckill_id&quot;</code>语句其实是<code>s.seckill_id as &quot;seckill.seckill_id&quot;</code>，这里省略了as（别名）；而<code>INNER JOIN</code>语句正是查询若两张表中中又相同字段的匹配值就根据两张表关联字段查询两张表的数据。这也可以使用<code>&lt;resultMap&gt;</code>中的<code>&lt;association&gt;</code>标签来实现，用于查询两张关联表的数据，如：</p>\n<pre><code class=\"lang-xml\">  &lt;resultMap id=&quot;findById&quot; type=&quot;SeckillOrder&quot;&gt;\n      &lt;id column=&quot;seckill_id&quot; property=&quot;seckillId&quot;/&gt;\n      &lt;result column=&quot;user_phone&quot; property=&quot;userPhone&quot;/&gt;\n      ...\n      &lt;association property=&quot;seckill&quot; javaType=&quot;Seckill&quot;&gt;\n          &lt;id column=&quot;seckill_id&quot; property=&quot;seckillId&quot;/&gt;\n          &lt;result column=&quot;title&quot; property=&quot;title&quot;/&gt;\n          ...\n      &lt;/association&gt;\n  &lt;/resultMap&gt;\n</code></pre>\n<p>如以上也是一种映射另外一张表数据的方式（当然使用这种方式在写SQL的时候需要指定限制条件<code>where s.seckill_id = so.seckill_id</code>强调两张表中的<code>seckill_id</code>字段值相同）。</p>\n<h1 id=\"h1-u6D4Bu8BD5\"><a name=\"测试\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>测试</h1><p>在编写了Mybatis的映射接口和XML映射文件，我们可以编写一个测试类来测试一下接口和XML配置是否正确。由于我们使用IDEA开发工具，打开接口文件用快捷键<code>Alt + Enter</code>（我这里用的Mac系统）显示一个面板，选择<code>Create Test</code>快速创建本文件的测试类。</p>\n<p>由于使用的SpringBoot框架，新创建的测试类位于<code>/src/test/java/</code>目录下，我们举例说明，比如创建<code>SeckillMapper</code>接口的测试文件：SeckillMapperTest.java</p>\n<pre><code class=\"lang-java\">public class SeckillMapperTest {\n\n    @Autowired\n    private SeckillMapper seckillMapper;\n\n    @Test\n    public void findAll() {\n    }\n\n    @Test\n    public void findById() {\n    }\n\n    @Test\n    public void reduceStock() {\n    }\n}\n</code></pre>\n<p>以上就是使用IDEA快捷键创建的测试类，我们仅以<code>findAll()</code>方法举例说明一下如何使用SpringBoot的测试类。如下：</p>\n<p>此处的源码请参看：<a href=\"https://github.com/TyCoding/springboot-seckill/tree/master/src/test/java/cn/tycoding/mapper\">Github</a></p>\n<pre><code class=\"lang-java\">@RunWith(SpringJUnit4ClassRunner.class)\n//@ContextConfiguration(&quot;classpath:application.yml&quot;)\n@SpringBootTest\npublic class SeckillMapperTest {\n\n    @Autowired\n    private SeckillMapper seckillMapper;\n\n    @Test\n    public void findAll() {\n        List&lt;Seckill&gt; all = seckillMapper.findAll();\n        for (Seckill seckill : all) {\n            System.out.println(seckill.getTitle());\n        }\n    }\n\n    @Test\n    public void findById() {\n    }\n\n    @Test\n    public void reduceStock() {\n    }\n}\n</code></pre>\n<p>SpringBoot的测试类和传统Spring框架测试类的最大区别就是不再使用<code><a href=\"https://github.com/ContextConfiguration\" title=\"&#64;ContextConfiguration\" class=\"at-link\">@ContextConfiguration</a>()</code>注解去加载配置文件，取而代之的是使用<code><a href=\"https://github.com/SpringBootTest\" title=\"&#64;SpringBootTest\" class=\"at-link\">@SpringBootTest</a></code>注解。因为SpringBoot已经严格规定了配置文件放在<code>resources</code>目录下，且一般是<code>.properties</code>或<code>.yml</code>结尾。如果你再使用<code><a href=\"https://github.com/ContextConfiguration\" title=\"&#64;ContextConfiguration\" class=\"at-link\">@ContextConfiguration</a>()</code>注解加载配置文件反而会报错。</p>\n<p><br/></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-42c33f0dc21862ee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-9c1b3f27bcc854a3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-44b0b34e98d037ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><br/></p>\n<h1 id=\"h1-u4EA4u6D41\"><a name=\"交流\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>交流</h1><p>如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！</p>\n<p><br/></p>\n<h1 id=\"h1-u8054u7CFB\"><a name=\"联系\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>联系</h1><p>If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.</p>\n<ul>\n<li><a href=\"http://www.tycoding.cn\"\">Blog&#64;TyCoding’s blog</a></li><li><a href=\"https://github.com/TyCoding\"\">GitHub&#64;TyCoding</a></li><li><a href=\"https://www.zhihu.com/people/tomo-83-82/activities\"\">ZhiHu&#64;TyCoding</a></li></ul>\n', '**秒杀**系统在如今电商项目中是很常见的，最近在学习电商项目时讲到了秒杀系统的实现，于是打算使用SpringBoot框架学习一下秒杀系统（本项目基于慕课网的一套免费视频教程：[Java高并发秒杀API](https://www.imooc.com/u/2145618/courses?sort=publish)，视频教程中讲解的很详细，非常感谢这位讲师）。也是因为最近学习了SpringBoot框架（GitHub教程：[SpringBoot入门之CRUD](https://github.com/TyCoding/spring-boot) ），觉得SpringBoot框架确实比传统SSM框架方便了很多，于是更深层次练习使用SpringBoot框架，注意：**SpringBoot不是对Spring功能上的增强，而是提供了一种快速使用Spring的方式。** 如果你熟悉了SSM框架，学习SpringBoot框架也是很Easy的。\n\n本项目的源码请参看：[springboot-seckill](https://github.com/TyCoding/springboot-seckill)  如果觉得不错可以star一下哦(#^.^#)\n\n\n<!--more-->\n\n本项目一共分为四个模块来讲解，具体的开发教程请看我的博客文章：\n\n* [SpringBoot实现Java高并发秒杀系统之DAO层开发（一）](http://tycoding.cn/2018/10/12/seckill-dao/)\n\n* [SpringBoot实现Java高并发秒杀系统之Service层开发（二）](http://tycoding.cn/2018/10/13/seckill-service/)\n\n* [SpringBoot实现Java高并发秒杀系统之Web层开发（三）](http://tycoding.cn/2018/10/14/seckill-web/)\n\n* [SpringBoot实现Java高并发秒杀系统之并发优化（四）](http://tycoding.cn/2018/10/15/seckill/)\n\n\n# 起步\n\n首先我们需要搭建SpringBoot项目开发环境，IDEA搭建SpringBoot项目的具体教程请看我的：[博文](http://tycoding.cn/2018/09/30/springboot-mybatis/#more)。\n\n如果你对SpringBoot框架或是SSM框架不熟悉，我想推荐一下我的几个小项目帮助你更好的理解：\n\n* [SpringBoot起步之环境搭建](http://tycoding.cn/2018/09/28/spring-boot/)\n\n* [SpringBoot-Mybatis入门之CRUD](http://tycoding.cn/2018/09/30/springboot-mybatis/#more)\n\n* [手把手教你整合SSM框架](https://github.com/TyCoding/ssm)\n\n* [SSM框架入门之环境搭建](http://tycoding.cn/2018/06/04/ssm/)\n\n<br/>\n\n**项目设计**\n\n```\n.\n├── README  -- Doc文档\n├── db  -- 数据库约束文件\n├── mvnw\n├── mvnw.cmd\n├── pom.xml  -- 项目依赖\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── cn\n    │   │       └── tycoding\n    │   │           ├── SpringbootSeckillApplication.java  -- SpringBoot启动器\n    │   │           ├── controller  -- MVC的web层\n    │   │           ├── dto  -- 统一封装的一些结果属性，和entity类似\n    │   │           ├── entity  -- 实体类\n    │   │           ├── enums  -- 手动定义的字典枚举参数\n    │   │           ├── exception  -- 统一的异常结果\n    │   │           ├── mapper  -- Mybatis-Mapper层映射接口，或称为DAO层\n    │   │           ├── redis  -- redis,jedis 相关配置\n    │   │           └── service  -- 业务层\n    │   └── resources\n    │       ├── application.yml  -- SpringBoot核心配置\n    │       ├── mapper  -- Mybatis-Mapper层XML映射文件\n    │       ├── static  -- 存放页面静态资源，可通过浏览器直接访问\n    │       │   ├── css\n    │       │   ├── js\n    │       │   └── lib\n    │       └── templates  -- 存放Thymeleaf模板引擎所需的HTML，不能在浏览器直接访问\n    │           ├── page\n    │           └── public  -- HTML页面公共组件（头部、尾部）\n    └── test  -- 测试文件\n```\n\n## SpringBoot\n\n之前我们在[SpringBoot-Mybatis入门之CRUD](https://github.com/TyCoding/spring-boot)中已经详细讲解了SpringBoot框架的开发流程，还是觉得一句话说的特别好：**SpringBoot不是对对Spring功能上的增强，而是提供了一种快速使用Spring的方式**。所以用SSM阶段的知识足够了SpringBoot阶段的开发，下面我们强调一下小技巧：\n\n* SpringBoot不需要配置注解扫描，之前我们配置`<context:component-scan>`扫描可能使用注解(@Service,@Component,@Controller等)的包路径。默认创建SpringBoot项目自动生成的Application.java启动器类会自动扫描其下的所有注解。\n\n* SpringBoot项目中静态资源都放在`resources`目录下，其中`static`目录中的数据可以直接通过浏览器访问，多用来放CSS、JS、img，但是不用来放html页面；其中`templates`用来存放HTML页面，但是需要在SpringBoot的配置文件(application.yml)中配置`spring.thymeleaf.prefix`标识Thymeleaf模板引擎渲染的页面位置。\n\n* HTML页面通过Thymeleaf的加持，为HTML页面赋予了很多功能，此时的HTML页面类似于JSP页面。访问后端存入域对象(session,request...)中的数据，可以通过`th:text=\"${key}\"`获得，在JS中也可以通过`[[${key}]]`获得。\n\n* Thymeleaf提供了类似JSP页面`<include>`的功能：public-component:`<div th:fragment=\"header\">`，main-component:`<div th:replace=\"path/header :: header\">`(其中`path`表示public-component相对于templates的路径，`/header`表示component文件名，最后的`header`表示`th:fragment`中定义的名称)。\n\n## pom依赖\n\n```xml\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>2.0.5.RELEASE</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <java.version>1.8</java.version>\n    </properties>\n\n    <dependencies>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-jdbc</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-thymeleaf</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.mybatis.spring.boot</groupId>\n            <artifactId>mybatis-spring-boot-starter</artifactId>\n            <version>1.3.2</version>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-devtools</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>mysql</groupId>\n            <artifactId>mysql-connector-java</artifactId>\n            <scope>runtime</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- alibaba的druid数据库连接池 -->\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>druid-spring-boot-starter</artifactId>\n            <version>1.1.9</version>\n        </dependency>\n\n        <!-- redis客户端 -->\n        <dependency>\n            <groupId>redis.clients</groupId>\n            <artifactId>jedis</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>4.12</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n```\n\n## JavaBean实体类配置\n\n此处源码请看：[GitHub](https://github.com/TyCoding/springboot-seckill/tree/master/src/main/java/cn/tycoding/entity)\n\nSeckill.java\n\n```java\npublic class Seckill implements Serializable {\n\n    private long seckillId; //商品ID\n    private String title; //商品标题\n    private String image; //商品图片\n    private BigDecimal price; //商品原价格\n    private BigDecimal costPrice; //商品秒杀价格\n\n    @DateTimeFormat(pattern = \"yyyy-MM-dd HH:mm:ss\")\n    @JsonFormat(pattern = \"yyyy-MM-dd HH:mm:ss\", timezone = \"GMT+8\")\n    private Date createTime; //创建时间\n\n    @DateTimeFormat(pattern = \"yyyy-MM-dd HH:mm:ss\")\n    @JsonFormat(pattern = \"yyyy-MM-dd HH:mm:ss\", timezone = \"GMT+8\")\n    private Date startTime; //秒杀开始时间\n\n    @DateTimeFormat(pattern = \"yyyy-MM-dd HH:mm:ss\")\n    @JsonFormat(pattern = \"yyyy-MM-dd HH:mm:ss\", timezone = \"GMT+8\")\n    private Date endTime; //秒杀结束时间\n\n    private long stockCount; //剩余库存数量\n}\n```\n\nSeckillOrder.java\n\n```java\npublic class SeckillOrder implements Serializable {\n\n    private long seckillId; //秒杀到的商品ID\n    private BigDecimal money; //支付金额\n\n    private long userPhone; //秒杀用户的手机号\n\n    @DateTimeFormat(pattern = \"yyyy-MM-dd HH:mm:ss\")\n    @JsonFormat(pattern = \"yyyy-MM-dd HH:mm:ss\", timezone = \"GMT+8\")\n    private Date createTime; //创建时间\n\n    private boolean status; //订单状态， -1:无效 0:成功 1:已付款\n\n    private Seckill seckill; //秒杀商品，和订单是一对多的关系\n}\n```\n\n注意实体类中`Date`类型数据都用了`@DateTimeFormat()`(来自springframework)和`@JsonFormat()`(来自jackson)标识可以实现Controller在返回JSON数据（用`@ResponseBody`标识的方法或`@RestController`标识的类）的时候能将Date类型的参数值（经Mybatis查询得到的数据是英文格式的日期，因为实体类中是Date类型）转换为注解中指定的格式返回给页面（相当于经过了一层SimpleDateFormate）。\n\n其次要**注意**在编写实体类的时候尽量养成习惯**继承Serializable接口**。在`SeckillOrder`中我们注入了`Seckill`类作为一个属性，目的是为了可以使用多表查询的方式从`seckill_order`表中查询出来对应的`seckill`表数据。\n\n# 表设计\n\n创建完成了SpringBoot项目，首先我们需要初始化数据库，秒杀系统的建表SQL如下：\n\n```sql\n/*\n *  mysql-v: 5.7.22\n */\n\n-- 创建数据库\n-- CREATE DATABASE seckill DEFAULT CHARACTER SET utf8;\n\nDROP TABLE IF EXISTS `seckill`;\nDROP TABLE IF EXISTS `seckill_order`;\n\n-- 创建秒杀商品表\nCREATE TABLE `seckill`(\n  `seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT \'商品ID\',\n  `title` varchar (1000) DEFAULT NULL COMMENT \'商品标题\',\n  `image` varchar (1000) DEFAULT NULL COMMENT \'商品图片\',\n  `price` decimal (10,2) DEFAULT NULL COMMENT \'商品原价格\',\n  `cost_price` decimal (10,2) DEFAULT NULL COMMENT \'商品秒杀价格\',\n  `stock_count` bigint DEFAULT NULL COMMENT \'剩余库存数量\',\n  `start_time` timestamp NOT NULL DEFAULT \'1970-02-01 00:00:01\' COMMENT \'秒杀开始时间\',\n  `end_time` timestamp NOT NULL DEFAULT \'1970-02-01 00:00:01\' COMMENT \'秒杀结束时间\',\n  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \'创建时间\',\n  PRIMARY KEY (`seckill_id`),\n  KEY `idx_start_time` (`start_time`),\n  KEY `idx_end_time` (`end_time`),\n  KEY `idx_create_time` (`end_time`)\n) CHARSET=utf8 ENGINE=InnoDB COMMENT \'秒杀商品表\';\n\n-- 创建秒杀订单表\nCREATE TABLE `seckill_order`(\n  `seckill_id` bigint NOT NULL COMMENT \'秒杀商品ID\',\n  `money` decimal (10, 2) DEFAULT NULL COMMENT \'支付金额\',\n  `user_phone` bigint NOT NULL COMMENT \'用户手机号\',\n  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT \'创建时间\',\n  `state` tinyint NOT NULL DEFAULT -1 COMMENT \'状态：-1无效 0成功 1已付款\',\n  PRIMARY KEY (`seckill_id`, `user_phone`) /*联合主键，保证一个用户只能秒杀一件商品*/\n) CHARSET=utf8 ENGINE=InnoDB COMMENT \'秒杀订单表\';\n```\n\n**解释**\n\n秒杀系统的表设计还是相对简单清晰的，这里我们只考虑秒杀系统的业务表，不涉及其他的表，所以整个系统主要涉及两张表：秒杀商品表、订单表。当然实际情况肯定不止这两张表（比如付款相关表，但是我们并未实现这个功能），也不止表中的这些字段。这里我们需要特别注意以下几点：\n\n**注意**\n\n* 1.我这里使用的Mysql版本是5.7.22，在Mysql5.7之后timestamp默认值不能再是`0000 00-00 00:00:00`，具体的介绍请看：[mysql官方文档](https://dev.mysql.com/doc/refman/5.7/en/datetime.html)。即 TIMESTAMP has a range of \'1970-01-01 00:00:01\' UTC to \'2038-01-19 03:14:07\' UTC.\n\n* 2.timestamp类型用来实现自动为新增行字段设置当前系统时间；且使用timestamp的字段必须给timestamp设置默认值，而在Mysql中date, datetime等类型都是无法实现默认设置当前系统时间值的功能(`DEFAULT CURRENT_TIMESTAMP`)的，所以我们必须使用timestamp类型，否则你要给字段传进来系统时间。\n\n* 3.decimal类型用于在数据库中设置精确的数值，比如`decimal(10,2)`表示可以存储10位且有2位小数的数值。\n\n* 4.tinyint类型用于存放int类型的数值，但是若用Mybatis作为DAO层框架，Mybatis会自动为tinyint类型的数据转换成true或false（0:false; 1 or 1+:true）。\n\n* 5.在订单表`seckill_order`中我们设计了联合主键：`PRIMARY KEY (seckill_id, user_phone)`，目的是为了避免单个用户重复购买同一件商品（一个用户只能秒杀到一次同一件商品）。\n\n* 6.无论是创建数据库还是创建表我们都应该养成一个习惯就是指定`character=utf-8`，避免中文数据乱码；其次还应该指定表的储存引擎是InnoDB，MySQL提供了两种储存引擎：InnoDB, MyISAM。但是只有InnoDB是支持事务的，且InnoDB相比MyISAM在并发上更具有高性能的优点。\n\n# DAO层开发\n\nDAO层是我们常说的三层架构（Web层-业务层-持久层）中与数据库交互的持久层，但是实际而言，架构是这样设计的，但是并不代表着实际项目中就一定存在一个`dao`文件夹，特别是现阶段我们使用的Spring-Mybatis框架。Mybatis提供了一种接口代理开发模式，也就是我们需要提供一个interface接口，其他和数据库交互的SQL编写放到对应的XML文件中（但是需要进行相关的数据库参数配置，并且Mybatis规定了使用这种开发模式必须保持接口和XML文件名称对应）。于是在本项目中就没有出现`dao`整个文件夹，取而代之的是`mapper`这个文件夹，我感觉更易识别出为Mybatis的映射接口文件。其实在实际项目中考虑到项目的大小和复杂程度，`dao`和`mapper`可能是同时存在的，因为service可能并不满足项目的设计，即为dao接口创建实现类，在实现类中再调用mapper接口来实现功能模块的扩展。\n\n<br/>\n\nDAO层开发，即DAO层接口开发，主要设计需要和数据库交互的数据有哪些？应该用什么返回值类型接收查询到的数据？所以包含的方法有哪些？带着这些问题，我们先看一下秒杀系统的业务流程：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-0bf43f030ee56ae9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n由上图可以看出，相对与本项目而言和数据库打交道的主要涉及两个操作：1.减库存（秒杀商品表）；2.记录购买明细（订单表）。\n\n* 减库存，顾名思义就是减少当前被秒杀到的商品的库存数量，这也是秒杀系统中一个处理难点的地方。实现减库存即count-1，但是我们需要考虑Mysql的事务特性引发的种种问题、需要考虑如何避免同一用户重复秒杀的行为。\n\n* 如果减库存的业务解决了那么记录购买明细的业务就相对简单很多了，我们需要记录购买用户的姓名、手机号、购买的商品ID等。因为本项目中不涉及支付功能，所以记录用户的购买订单的业务并不复杂。\n\n分析了上面的功能，下面我们开始DAO层接口的编写(源码请看：[GitHub](https://github.com/TyCoding/springboot-seckill/tree/master/src/main/java/cn/tycoding/mapper))：\n\n```java\n    /**\n     * 减库存。\n     * 对于Mapper映射接口方法中存在多个参数的要加@Param()注解标识字段名称，不然Mybatis不能识别出来哪个字段相互对应\n     *\n     * @param seckillId 秒杀商品ID\n     * @param killTime  秒杀时间\n     * @return 返回此SQL更新的记录数，如果>=1表示更新成功\n     */\n    int reduceStock(@Param(\"seckillId\") long seckillId, @Param(\"killTime\") Date killTime);\n\n    /**\n     * 插入购买订单明细\n     *\n     * @param seckillId 秒杀到的商品ID\n     * @param money     秒杀的金额\n     * @param userPhone 秒杀的用户\n     * @return 返回该SQL更新的记录数，如果>=1则更新成功\n     */\n    int insertOrder(@Param(\"seckillId\") long seckillId, @Param(\"money\") BigDecimal money, @Param(\"userPhone\") long userPhone);\n```\n\n但从接口设计上我们无非关注的就是这两个方法：1.减库存；2.插入购买明细。此处需要注意的是：\n\n* 对于SpringBoot系统，DAO（Mapper）层的接口需要使用`@Mapper`注解标识。因为SpringBoot系统中接口的XML文件不在`/java`目录下而是在`/resources`目录下。\n\n* 对于Mapper接口方法中存在传递多个参数的情况需要使用`@Param()`标识这个参数的名称，目的是为了帮助Mybatis识别传递的参数，不然Mybatis的XML中用的`#{}`不能识别出来你传递的参数名称是谁和谁对应的，类似于Controller层中常用的`@RequestParam()`注解。\n\n* **小技巧:** 之前我们做insert和update操作时直接用`void`作为方法返回值，实际上虽然Mybatis的`<update>`和`<select>`语句并没有`resultType`属性，但是并不代表其没有返回值，默认返回0或1，表示执行该SQL影响的行数。为此我们可以这样写SQL，如：`insert ignore into xxx`用来避免Mybatis报错，而是直接返回0表示当前SQL执行失败。\n\n* 小技巧：因为我们必须要避免同一个用户多次抢购同一件商品，在SQL中必须限制这一点（因为即使前端怎么控制都无法避免用户多次请求同一个接口，所谓接口防刷）。所以在设计订单表的时候用了联合主键且不自增的方式，以用户ID和用户电话组成联合主键，这样当同一个用户（电话相同）多次抢购同一件商品时插入的SQL就会产生主键冲突的问题，这样就会报错。\n\n## XML映射\n\n```xml\n    <update id=\"reduceStock\">\n        UPDATE seckill\n        SET stock_count = stock_count - 1\n        WHERE seckill_id = #{seckillId}\n        AND start_time &lt;= #{killTime}\n        AND end_time &gt;= #{killTime}\n        AND stock_count &gt; 0\n    </update>\n\n    <insert id=\"insertOrder\">\n        INSERT ignore INTO seckill_order(seckill_id, money, user_phone)\n        VALUES (#{seckillId}, #{money}, #{userPhone})\n    </insert>\n```\n\nSQL语句相对不是很复杂。减库存：执行update语句，令`stock_count`字段依次减一，并且当前要在一系列where条件的限制下；新增订单信息：保存订单数据，这里为接口防刷用联合主键`seckillId, userPhone`，如果同一个用户多次抢购同一件商品导致主键冲突会直接报错，为了避免系统不直接报错设计了`ignore`实现主键冲突就直接返回0表示该条SQL执行失败。\n\n**拓展**\n\n上面我使用了`&lt;`、`&gt;`的语法其实代表的是>= <=这种符号，因为在Mybatis中编写的SQL语句如果直接使用`>=`或`<=`这种判断条件可能会报错，我这里提供一种简单的解决方案就是用这种英文符号代替：\n\n| 原符号 | 替换符号 |\n| -- | -- |\n| <  | &lt; |\n| <= | &lt;= |\n| >  | &gt; |\n| >= | &gt;= |\n| & | &amp; |\n| \' | &apos; |\n| \" | &quot; |\n\n### order表中findById方法\n\n之前在`SeckillOrder.java`实体类中我们注入了`Seckill`属性，用于可以根据查询`seckill_order`表的同时查询到其对应的`seckill`表数据，对应的接口定义如下：\n\n```java\n    /**\n     * 根据秒杀商品ID查询订单明细数据并得到对应秒杀商品的数据，因为我们再SeckillOrder中已经定义了一个Seckill的属性\n     *\n     * @param seckillId\n     * @return\n     */\n    SeckillOrder findById(long seckillId);\n```\n\n对应的SQL如下：\n\n```xml\n    <select id=\"findById\" resultType=\"SeckillOrder\">\n        SELECT\n          so.seckill_id,\n          so.user_phone,\n          so.money,\n          so.create_time,\n          so.state,\n          s.seckill_id \"seckill.seckill_id\",\n          s.title \"seckill.title\",\n          s.cost_price \"seckill.cost_price\",\n          s.create_time \"seckill.create_time\",\n          s.start_time \"seckill.start_time\",\n          s.end_time \"seckill.end_time\",\n          s.stock_count \"seckill.stock_count\"\n        FROM seckill_order so\n        INNER JOIN seckill s ON so.seckill_id = s.seckill_id\n        WHERE so.seckill_id = #{seckillId}\n    </select>\n```\n\n这个SQL看似复杂些，但是就是仅仅的多表（两张表）查询语句：根据`seckill_order`表中的`seckill_id`字段查询`seckill`表中`seckill_id`字段值对应的数据（也就是说：对于多表查询，其实两张表之间必然存在一定的字段关联关系，不一定是外键关联，当然我们也不建议用外键关联两张表）。\n\n其中`findById`的SQL中类似`s.seckill_id \"seckill.seckill_id\"`语句其实是`s.seckill_id as \"seckill.seckill_id\"`，这里省略了as（别名）；而`INNER JOIN`语句正是查询若两张表中中又相同字段的匹配值就根据两张表关联字段查询两张表的数据。这也可以使用`<resultMap>`中的`<association>`标签来实现，用于查询两张关联表的数据，如：\n\n```xml\n  <resultMap id=\"findById\" type=\"SeckillOrder\">\n      <id column=\"seckill_id\" property=\"seckillId\"/>\n      <result column=\"user_phone\" property=\"userPhone\"/>\n      ...\n      <association property=\"seckill\" javaType=\"Seckill\">\n          <id column=\"seckill_id\" property=\"seckillId\"/>\n          <result column=\"title\" property=\"title\"/>\n          ...\n      </association>\n  </resultMap>\n```\n\n如以上也是一种映射另外一张表数据的方式（当然使用这种方式在写SQL的时候需要指定限制条件`where s.seckill_id = so.seckill_id`强调两张表中的`seckill_id`字段值相同）。\n\n\n# 测试\n\n在编写了Mybatis的映射接口和XML映射文件，我们可以编写一个测试类来测试一下接口和XML配置是否正确。由于我们使用IDEA开发工具，打开接口文件用快捷键`Alt + Enter`（我这里用的Mac系统）显示一个面板，选择`Create Test`快速创建本文件的测试类。\n\n由于使用的SpringBoot框架，新创建的测试类位于`/src/test/java/`目录下，我们举例说明，比如创建`SeckillMapper`接口的测试文件：SeckillMapperTest.java\n\n```java\npublic class SeckillMapperTest {\n\n    @Autowired\n    private SeckillMapper seckillMapper;\n\n    @Test\n    public void findAll() {\n    }\n\n    @Test\n    public void findById() {\n    }\n\n    @Test\n    public void reduceStock() {\n    }\n}\n```\n\n以上就是使用IDEA快捷键创建的测试类，我们仅以`findAll()`方法举例说明一下如何使用SpringBoot的测试类。如下：\n\n此处的源码请参看：[Github](https://github.com/TyCoding/springboot-seckill/tree/master/src/test/java/cn/tycoding/mapper)\n\n```java\n@RunWith(SpringJUnit4ClassRunner.class)\n//@ContextConfiguration(\"classpath:application.yml\")\n@SpringBootTest\npublic class SeckillMapperTest {\n\n    @Autowired\n    private SeckillMapper seckillMapper;\n\n    @Test\n    public void findAll() {\n        List<Seckill> all = seckillMapper.findAll();\n        for (Seckill seckill : all) {\n            System.out.println(seckill.getTitle());\n        }\n    }\n\n    @Test\n    public void findById() {\n    }\n\n    @Test\n    public void reduceStock() {\n    }\n}\n```\n\nSpringBoot的测试类和传统Spring框架测试类的最大区别就是不再使用`@ContextConfiguration()`注解去加载配置文件，取而代之的是使用`@SpringBootTest`注解。因为SpringBoot已经严格规定了配置文件放在`resources`目录下，且一般是`.properties`或`.yml`结尾。如果你再使用`@ContextConfiguration()`注解加载配置文件反而会报错。\n\n\n<br/>\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-42c33f0dc21862ee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-9c1b3f27bcc854a3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-44b0b34e98d037ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n<br/>\n\n# 交流\n\n如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！\n\n<br/>\n\n# 联系\n\nIf you have some questions after you see this article, you can contact me or you can find some info by clicking these links.\n\n- [Blog@TyCoding\'s blog](http://www.tycoding.cn)\n- [GitHub@TyCoding](https://github.com/TyCoding)\n- [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)\n', NULL, 'http://tycoding.cn', '1', 2140, '2018-08-17 12:03:53', '2018-10-17 12:03:53', '2018-10-17 12:03:53', 0);
INSERT INTO `tb_article` VALUES (5, 'INTRO', '/site/images/thumbs/10.jpg', '涂陌', '<p>hello!  I’m TyCoding. this my blog. I decide to write my own blog while I’m coding and want to use this blog to record my learning process.      </p>\n<p>So, Welcome to <a href=\"http://tycoding.cn\">TyCoding’s blog</a> website! </p>\n<p>Today I will begin my blog journey，and I will update it frequently.   </p>\n<!--more-->\n<p><br/></p>\n<h2 id=\"h2-u5173u4E8Eu6211\"><a name=\"关于我\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>关于我</h2><p>咸鱼一只，已然脱离了高考的滚滚浪潮，而今正在幻想美好的大学生活中开始新的旅程。</p>\n<p><br/></p>\n<h2 id=\"h2-u5173u4E8Eu535Au5BA2\"><a name=\"关于博客\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>关于博客</h2><p>主要用来记录一些学习笔记，供自己或是他人学习</p>\n<p>不定期分享自己小小的Demo</p>\n<p>或是一些作品</p>\n<p>请持续关注啦。</p>\n<p><br/></p>\n<h2 id=\"h2-u4EA4u6D41\"><a name=\"交流\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>交流</h2><p>如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！  </p>\n<p><br/></p>\n<h2 id=\"h2-u8054u7CFB\"><a name=\"联系\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>联系</h2><p>If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.</p>\n<ul>\n<li><a href=\"http://www.tycoding.cn\"\">Blog&#64;TyCoding’s blog</a></li><li><a href=\"https://github.com/TyCoding\"\">GitHub&#64;TyCoding</a></li><li><a href=\"https://www.zhihu.com/people/tomo-83-82/activities\"\">ZhiHu&#64;TyCoding</a></li></ul>\n<p><br/></p>\n<h2 id=\"h2-u6700u540E\"><a name=\"最后\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>最后</h2><p>Sweet     —— DcAlvis</p>\n<div style=\"position:absolute; width:400px;\"><iframe width=\"400px\" style=\"width:400px;\" frameborder=\"no\" border=\"0\" marginwidth=\"0\" marginheight=\"0\" src=\"//music.163.com/outchain/player?type=2&id=484278264&auto=1&height=66\"></iframe></div>\n\n<p><br/></p>\n<p><br/></p>\n', '\n\n\nhello!  I\'m TyCoding. this my blog. I decide to write my own blog while I\'m coding and want to use this blog to record my learning process.      \n\nSo, Welcome to [TyCoding\'s blog](http://tycoding.cn) website! \n\nToday I will begin my blog journey，and I will update it frequently.   \n\n\n\n<!--more-->\n\n<br/>\n\n## 关于我\n\n咸鱼一只，已然脱离了高考的滚滚浪潮，而今正在幻想美好的大学生活中开始新的旅程。\n\n<br/>\n\n## 关于博客\n\n主要用来记录一些学习笔记，供自己或是他人学习\n\n不定期分享自己小小的Demo\n\n或是一些作品\n\n请持续关注啦。\n\n<br/>\n\n## 交流\n\n如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！  \n\n<br/>\n\n## 联系\n\nIf you have some questions after you see this article, you can contact me or you can find some info by clicking these links.\n\n* [Blog@TyCoding\'s blog](http://www.tycoding.cn)\n* [GitHub@TyCoding](https://github.com/TyCoding)\n* [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)\n\n\n<br/>\n\n\n## 最后\n\nSweet     —— DcAlvis\n\n<div style=\"position:absolute; width:400px;\"><iframe width=\"400px\" style=\"width:400px;\" frameborder=\"no\" border=\"0\" marginwidth=\"0\" marginheight=\"0\" src=\"//music.163.com/outchain/player?type=2&id=484278264&auto=1&height=66\"></iframe></div>\n\n<br/>\n\n<br/>\n', NULL, 'http://tycoding.cn', '1', 2002, '2018-02-01 00:00:01', '2018-02-01 00:00:01', '2018-11-01 12:24:10', 0);
INSERT INTO `tb_article` VALUES (6, 'SpringBoot整合Mybatis实现CRUD', '/site/images/thumbs/11.jpg', '涂小陌', '<p>继上篇文章：<a href=\"http://tycoding.cn/2018/09/28/spring-boot/\">Spring-Boot入门之环境搭建</a>。这次我们整合SpringBoot-Mybatis实现简单的CRUD业务。</p>\n<p>需求：</p>\n<ul>\n<li>详解SpringBoot工程的构建、与SSM项目在工程搭建上的不同。</li><li>实现SpringBoot-Mybatis整合征服数据库。</li><li>解决页面跳转，详解与SSM阶段的不同。</li><li>实现分页查询，使用PaheHelper插件和ElementUI分页控件。</li><li>实现文件上传。</li><li>使用Spring AOP切面编程实现简易的实现登录拦截工程。</li></ul>\n<p><strong>项目源码</strong>请看我的Github仓库：<a href=\"https://github.com/TyCoding/spring-boot\">教你优雅的入门Spring Boot框架</a></p>\n<p><strong>如果觉得不错就点击右上角star鼓励一下笔者吧(#^.^#)</strong></p>\n<h1 id=\"h1--spring-boot-\"><a name=\"教你优雅的入门Spring Boot框架\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>教你优雅的入门Spring Boot框架</h1><p><strong>技术栈</strong></p>\n<ul>\n<li>后端： SpringBoot + Mybatis</li><li>前端： Vue.JS + ElementUI</li></ul>\n<p><strong>测试环境</strong></p>\n<ul>\n<li>IDEA + SpringBoot-2.0.5</li></ul>\n<p><strong>项目设计</strong></p>\n<pre><code>.\n├── db  -- sql文件\n├── mvnw \n├── mvnw.cmd\n├── pom.xml  -- 项目依赖\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── cn\n    │   │       └── tycoding\n    │   │           ├── SpringbootApplication.java  -- Spring Boot启动类\n    │   │           ├── controller  -- MVC-WEB层\n    │   │           ├── entity  -- 实体类\n    │   │           ├── interceptor  -- 自定义拦截器\n    │   │           ├── mapper  -- mybatis-Mapper层接口\n    │   │           └── service  -- service业务层\n    │   └── resources  -- Spring Boot资源文件 \n    │       ├── application.yml  -- Spring Boot核心配置文件\n    │       ├── mapper  -- Mybatis Mapper层配置文件\n    │       ├── static  -- 前端静态文件\n    │       └── templates  -- Thymeleaf模板引擎识别的HTML页面目录\n    └── test  -- 测试文件\n</code></pre><h1 id=\"h1-u51C6u5907\"><a name=\"准备\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>准备</h1><p>开始实战Spring Boot项目，首先，你需要将Spring Boot工程搭建出来。</p>\n<p>Spring Boot工程搭建请看我的博客：<a href=\"http://tycoding.cn/2018/09/28/spring-boot/\">Spring Boot入门之工程搭建</a></p>\n<h2 id=\"h2-spring-boot-\"><a name=\"Spring Boot应用启动器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot应用启动器</h2><p>Spring Boot提供了很多应用启动器，分别用来支持不同的功能，说白了就是<code>pom.xml</code>中的依赖配置，因为Spring Boot的自动化配置特性，我们并不需再考虑项目依赖版本问题，使用Spring Boot的应用启动器，它能自动帮我们将相关的依赖全部导入到项目中。</p>\n<p>我们这里介绍几个常见的应用启动器：</p>\n<ul>\n<li><code>spring-boot-starter</code>: Spring Boot的核心启动器，包含了自动配置、日志和YAML</li><li><code>spring-boot-starter-aop</code>: 支持AOP面向切面编程的功能，包括spring-aop和AspecJ</li><li><code>spring-boot-starter-cache</code>: 支持Spring的Cache抽象</li><li><code>spring-boot-starter-artermis</code>: 通过Apache Artemis支持JMS（Java Message Service）的API</li><li><code>spring-boot-starter-data-jpa</code>: 支持JPA</li><li><code>spring-boot-starter-data-solr</code>: 支持Apache Solr搜索平台，包括spring-data-solr</li><li><code>spring-boot-starter-freemarker</code>: 支持FreeMarker模板引擎</li><li><code>spring-boot-starter-jdbc</code>: 支持JDBC数据库</li><li><code>spring-boot-starter-Redis</code>: 支持Redis键值储存数据库，包括spring-redis</li><li><code>spring-boot-starter-security</code>: 支持spring-security</li><li><code>spring-boot-starter-thymeleaf</code>: 支持Thymeleaf模板引擎，包括与Spring的集成</li><li><code>spring-boot-starter-web</code>: 支持全栈式web开发，包括tomcat和Spring-WebMVC</li><li><code>spring-boot-starter-log4j</code>: 支持Log4J日志框架</li><li><code>spring-boot-starter-logging</code>: 引入Spring Boot默认的日志框架Logback</li></ul>\n<h2 id=\"h2-spring-boot-\"><a name=\"Spring Boot项目结构设计\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Spring Boot项目结构设计</h2><p>Spring Boot项目（即Maven项目），当然拥有最基础的Maven项目结构。除此之外：</p>\n<ol>\n<li>Spring Boot项目中不包含webapp(webroot)目录。</li><li>Spring Boot默认提供的静态资源目录需要置于classpath下，且其下的目录名称要符合一定规定。</li><li>Spring Boot默认不提倡用XML配置文件，主张使用YML作为配置文件格式，YML有更简洁的语法。当然也可以使用.properties作为配置文件格式。</li><li>Spring Boot官方推荐使用Thymeleaf作为前端模板引擎，并且Thymeleaf默认将templates作为静态页面的存放目录（由配置文件指定）。</li><li><p>Spring Boot默认将<code>resources</code>作为静态资源的存放目录，存放前端静态文件、项目配置文件。</p>\n</li><li><p>Spring Boot规定<code>resources</code>下的子级目录名要符合一定规则，一般我们设置<code>resources/static</code>为前端静态（JS,CSS）的存放目录；设置<code>resources/templates</code>作为HTML页面的存放目录。</p>\n</li><li><p>Spring Boot指定的Thymeleaf模板引擎文件目录<code>/resources/templates</code>是受保护的目录，想当与之前的WEB-INF文件夹，里面的静态资源不能直接访问，一般我们通过Controller映射访问。</p>\n</li><li><p>建议将Mybatis-Mapper的XML映射文件放于<code>resources/</code>目录下，我这里设为<code>resources/mapper</code>目录，且<code>src/main/java/Mapper</code>下的Mapper层接口要使用<code><a href=\"https://github.com/Mapper\" title=\"&#64;Mapper\" class=\"at-link\"><a href=\"https://github.com/Mapper\" title=\"&#64;Mapper\" class=\"at-link\">@Mapper</a></a></code>注解标识，不然mybatis找不到接口对应的XML映射文件。</p>\n</li><li><p><code>SpringBootApplication.java</code>为项目的启动器类，项目不需要部署到Tomcat上，由SpringBoot提供的服务器部署项目（运行启动器类即可）；且SpringBoot会自动扫描该启动器同级和子级下用注解标识的Bean。</p>\n</li><li><p>Spring Boot不建议使用JSP页面，如果想使用，请自行百度解决办法。</p>\n</li><li><p>上面说了Spring Boot提供的存放HTML静态页面的目录<code>resources/templates</code>是受保护的目录，访问其中的HTML页面要通过Controller映射，这就间接规定了你需要配置Spring的视图解析器，且Controller类不能使用<code><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\"><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\">@RestController</a></a></code>标识。</p>\n</li></ol>\n<h1 id=\"h1-u8D77u6B65\"><a name=\"起步\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>起步</h1><p><em>首先：</em>  <strong>我想特殊强调的是：SpringBoot不是对Spring功能上的增强，而是提供了一种快速使用Spring的方式</strong>。一定要切记这一点。</p>\n<p>学习SpringBoot框架，只是为了更简便的使用Spring框架，我们在SSM阶段学习的知识现在放在Spring Boot框架上开发是完全适用的，我们学习的大多数是SpringBoot的自动化配置方式。</p>\n<p>因为Spring Boot框架的一大优势就是自动化配置，从pom.xml的配置中就能明显感受到。</p>\n<p>所以这里推荐一下我之前的SSM阶段整合项目： <a href=\"https://github.com/TyCoding/ssm\">SSM详细入门整合案例</a>    <a href=\"https://github.com/TyCoding/ssm-redis-solr\">SSM+Redis+Shiro+Solr+Vue.js整合项目</a></p>\n<h2 id=\"h2-u9879u76EEu4F9Du8D56\"><a name=\"项目依赖\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>项目依赖</h2><p>本项目的依赖文件请看Github仓库：<a href=\"https://github.com/TyCoding/spring-boot/blob/master/pom.xml\">spring-boot/pom.xml</a></p>\n<h2 id=\"h2-u521Du59CBu5316u6570u636Eu5E93\"><a name=\"初始化数据库\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>初始化数据库</h2><p>本项目数据库表设计请看GitHub仓库：<a href=\"https://github.com/TyCoding/spring-boot/tree/master/db\">spring-boot/db/</a></p>\n<p>请运行项目前，先把数据库表结构建好</p>\n<h2 id=\"h2-springboot-mybatis\"><a name=\"SpringBoot整合Mybatis\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>SpringBoot整合Mybatis</h2><p>之前已经说过：<strong>SpringBoot框架不是对Spring功能上的增强，而是提供了一种快速使用Spring的方式</strong></p>\n<p>所以说，SpringBoot整合Mybatis的思想和Spring整合Mybatis的思想基本相同，不同之处有两点：</p>\n<ul>\n<li><p>1.Mapper接口的XML配置文件变化。之前我们使用Mybatis接口代理开发，规定Mapper映射文件要和接口在一个目录下；而这里Mapper映射文件置于<code>resources/mapper/</code>下，且置于<code>src/main/java/</code>下的Mapper接口需要用<code><a href=\"https://github.com/Mapper\" title=\"&#64;Mapper\" class=\"at-link\"><a href=\"https://github.com/Mapper\" title=\"&#64;Mapper\" class=\"at-link\">@Mapper</a></a></code>注解标识，不然映射文件与接口无法匹配。</p>\n</li><li><p>2.SpringBoot建议使用YAML作为配置文件，它有更简便的配置方式。所以整合Mybatis在配置文件上有一定的区别，但最终都是那几个参数的配置。</p>\n</li></ul>\n<p>关于YAML的语法请自行百度，我这里也仅仅是满足基本的配置需求，不涉及那种不易理解的语法。</p>\n<h3 id=\"h3-u6574u5408u914Du7F6Eu6587u4EF6\"><a name=\"整合配置文件\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>整合配置文件</h3><p>本例详细代码请看GitHub仓库：<a href=\"https://github.com/TyCoding/spring-boot/blob/master/src/main/resources/application.yml\">spring-boot/resources/application.yml</a></p>\n<p>在Spring阶段用XML配置mybatis无非就是配置：1.连接池；2.数据库url连接；3.mysql驱动；4.其他初始化配置</p>\n<pre><code class=\"lang-YAML\">spring:\n  datasource:\n    name: springboot\n    type: com.alibaba.druid.pool.DruidDataSource\n    #druid相关配置\n    druid:\n      #监控统计拦截的filters\n      filter: stat\n      #mysql驱动\n      driver-class-name: com.mysql.jdbc.Driver\n      #基本属性\n      url: jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=true&amp;characterEncoding=UTF-8&amp;allowMultiQueries=true\n      username: root\n      password: root\n      #配置初始化大小/最小/最大\n      initial-size: 1\n      min-idle: 1\n      max-active: 20\n      #获取连接等待超时时间\n      max-wait: 60000\n      #间隔多久进行一次检测，检测需要关闭的空闲连接\n      time-between-eviction-runs-millis: 60000\n\n  #mybatis配置\n  mybatis:\n    mapper-locations: classpath:mapper/*.xml\n    type-aliases-package: cn.tycoding.entity\n</code></pre>\n<p><strong>注意：空格代表节点层次；注释部分用<code>#</code>标记</strong></p>\n<p><strong>解释</strong></p>\n<ol>\n<li><p>我们实现的是spring-mybatis的整合，包含mybatis的配置以及datasource数据源的配置当然属于spring配置中的一部分，所以需要在<code>spring:</code>下。</p>\n</li><li><p><code>mapper-locations</code>相当于XML中的<code>&lt;property name=&quot;mapperLocations&quot;&gt;</code>用来扫描Mapper层的配置文件，由于我们的配置文件在<code>resources</code>下，所以需要指定<code>classpath:</code>。</p>\n</li><li><p><code>type-aliases-package</code>相当与XML中<code>&lt;property name=&quot;typeAliasesPackase&quot;&gt;</code>别名配置，一般取其下实体类类名作为别名。</p>\n</li><li><p><code>datasource</code>数据源的配置，<code>name</code>表示当前数据源的名称，类似于之前的<code>&lt;bean id=&quot;dataSource&quot;&gt;</code>id属性，这里可以任意指定，因为我们无需关注Spring是怎么注入这个Bean对象的。</p>\n</li><li><p><code>druid</code>代表本项目中使用了阿里的druid连接池，<code>driver-class-name:</code>相当于XML中的<code>&lt;property name=&quot;driverClassName&quot;&gt;</code>；<code>url</code>代表XML中的<code>&lt;property name=&quot;url&quot;&gt;</code>；<code>username</code>代表XML中的<code>&lt;property name=&quot;username&quot;&gt;</code>；<code>password</code>代表XML中的<code>&lt;property name=&quot;password&quot;&gt;</code>；其他druid的私有属性配置不再解释。这里注意druid连接池和c3p0连接池在XML的<property>的name中就不同，在此处SpringBoot的配置中当然名称也不同。</p>\n</li></ol>\n<p>如果Spring整合Mybtis的配置你已经很熟悉了，那么这个配置你肯定也很眼熟，从英文名称上就很容易区分出来。这里需要注意的就是YAML语法规定不同行空格代表了不同的层级结构。</p>\n<p>既然完成了SpringBoot-Mybatis基本配置下面我们实战讲解如何实现基本的CRUD。</p>\n<h3 id=\"h3-u5B9Eu73B0u67E5u8BE2\"><a name=\"实现查询\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现查询</h3><blockquote>\n<p>1.在<code>src/main/java/cn/tycoding/entity/</code>下新建<code>User.java</code>实体类</p>\n</blockquote>\n<pre><code class=\"lang-java\">public class User implements Serializable {\n    private Long id; //编号\n    private String username; //用户名\n    private String password; //密码\n    //getter/setter\n}\n</code></pre>\n<blockquote>\n<p>2.在<code>src/main/java/cn/tycoding/service/</code>下创建<code>BaseService.java</code>通用接口，目的是简化service层接口基本CRUD方法的编写。</p>\n</blockquote>\n<pre><code class=\"lang-java\">public interface BaseService&lt;T&gt; {\n\n    // 查询所有\n    List&lt;T&gt; findAll();\n\n    //根据ID查询\n    List&lt;T&gt; findById(Long id);\n\n    //添加\n    void create(T t);\n\n    //删除（批量）\n    void delete(Long... ids);\n\n    //修改\n    void update(T t);\n}\n</code></pre>\n<p>以上就是我对Service层基本CRUD接口的简易封装，使用了泛型类，其继承接口指定了什么泛型，T就代表什么类。</p>\n<blockquote>\n<p>3.在<code>src/main/java/cn/tycoding/service/</code>下创建<code>UserService.java</code>接口：</p>\n</blockquote>\n<pre><code class=\"lang-java\">public interface UserService extends BaseService&lt;User&gt; {}\n</code></pre>\n<blockquote>\n<p>4.在<code>src/main/java/cn/tycoding/service/impl/</code>下创建<code>UserServiceImpl.java</code>实现类：</p>\n</blockquote>\n<pre><code class=\"lang-java\">@Service\npublic class UserServiceImpl implements UserService {\n\n    @Autowired\n    private UserMapper userMapper;\n\n    @Override\n    public List&lt;User&gt; findAll() {\n        return userMapper.findAll();\n    }\n\n    //其他方法省略\n}\n</code></pre>\n<blockquote>\n<p>5.在<code>src/main/java/cn/tycoding/mapper/</code>下创建<code>UserMapper.java</code>Mapper接口类：</p>\n</blockquote>\n<pre><code class=\"lang-java\">@Mapper\npublic interface UserMapper {\n    List&lt;User&gt; findAll();\n}\n</code></pre>\n<p>如上，我们一定要使用<code><a href=\"https://github.com/Mapper\" title=\"&#64;Mapper\" class=\"at-link\">@Mapper</a></code>接口标识这个接口，不然Mybatis找不到其对应的XML映射文件。</p>\n<blockquote>\n<p>6.在<code>src/main/resources/mapper/</code>下创建<code>UserMapper.xml</code>映射文件：</p>\n</blockquote>\n<pre><code class=\"lang-xml\">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;\n&lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot; &gt;\n&lt;mapper namespace=&quot;cn.tycoding.mapper.UserMapper&quot;&gt;\n\n    &lt;!-- 查询所有 --&gt;\n    &lt;select id=&quot;findAll&quot; resultType=&quot;cn.tycoding.entity.User&quot;&gt;\n        SELECT * FROM tb_user\n    &lt;/select&gt;\n&lt;/mapper&gt;\n</code></pre>\n<blockquote>\n<p>7.在<code>src/main/java/cn/tycoding/controller/admin/</code>下创建<code>UserController.java</code></p>\n</blockquote>\n<pre><code class=\"lang-java\">@RestController\npublic class UserController {\n    @Autowired\n    private UserService userService;\n\n    @RequestMapping(&quot;/findAll&quot;)\n    public List&lt;User&gt; findAll() {\n        return userService.findAll();\n    }\n}\n</code></pre>\n<blockquote>\n<p>8.运行<code>src/main/java/cn/tycoding/SpringbootApplication.java</code>的main方法，启动springboot</p>\n</blockquote>\n<p>在浏览器上访问<code>localhost:8080/findAll</code>即可得到一串JSON数据。</p>\n<h3 id=\"h3-u601Du8003\"><a name=\"思考\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>思考</h3><p>看了上面一步步的讲解。你应该明白了，其实和SSM阶段的CRUD基本相同，这里我就不再举例其他方法。</p>\n<p>下面我们讲解一下不同的地方：</p>\n<h2 id=\"h2-u5B9Eu73B0u9875u9762u8DF3u8F6C\"><a name=\"实现页面跳转\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现页面跳转</h2><p>因为Thymeleaf指定的目录<code>src/main/resources/templates/</code>是受保护的目录，其下的资源不能直接通过浏览器访问，可以使用Controller映射的方式访问，怎么映射呢？</p>\n<blockquote>\n<p>1.在application.yml中添加配置</p>\n</blockquote>\n<pre><code class=\"lang-yaml\">spring:\n  thymeleaf:\n    prefix: classpath:/templates/\n    check-template-location: true\n    suffix: .html\n    encoding: UTF-8\n    mode: LEGACYHTML5\n    cache: false\n</code></pre>\n<p>指定Thymeleaf模板引擎扫描<code>resources</code>下的<code>templates</code>文件夹中已<code>.html</code>结尾的文件。这样就实现了MVC中关于视图解析器的配置：</p>\n<pre><code class=\"lang-xml\">    &lt;!-- 配置视图解析器 --&gt;\n    &lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;&gt;\n        &lt;property name=&quot;prefix&quot; value=&quot;/&quot;/&gt;\n        &lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot;/&gt;\n    &lt;/bean&gt;\n</code></pre>\n<p>是不是感觉方便很多呢？但这里需要注意的是：<code>classpath:</code>后的目录地址一定要先加<code>/</code>，比如目前的<code>classpath:/templates/</code>。</p>\n<blockquote>\n<p>2.在Controller添加映射方法</p>\n</blockquote>\n<pre><code class=\"lang-java\">    @GetMapping(value = {&quot;/&quot;, &quot;/index&quot;})\n    public String index() {\n        return &quot;home/index&quot;;\n    }\n</code></pre>\n<p>这样，访问<code>localhost:8080/index</code>将直接跳转到<code>resources/templates/home/index.html</code>页面。</p>\n<h2 id=\"h2-u5B9Eu73B0u5206u9875u67E5u8BE2\"><a name=\"实现分页查询\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现分页查询</h2><p>首先我们需要在application.yml中配置pageHelper插件</p>\n<pre><code class=\"lang-yaml\">pagehelper:\n  pagehelperDialect: mysql\n  reasonable: true\n  support-methods-arguments: true\n</code></pre>\n<p>我这里使用了Mybatis的PageHelper分页插件，前端使用了ElementUI自带的分页插件：具体的教程请查看我的博客：<a href=\"http://tycoding.cn/2018/07/30/vue-6/\">SpringMVC+ElementUI实现分页查询</a></p>\n<p><strong>核心配置：</strong></p>\n<p><code>UserServiceImp.java</code></p>\n<pre><code class=\"lang-java\">    public PageBean findByPage(Goods goods, int pageCode, int pageSize) {\n        //使用Mybatis分页插件\n        PageHelper.startPage(pageCode, pageSize);\n\n        //调用分页查询方法，其实就是查询所有数据，mybatis自动帮我们进行分页计算\n        Page&lt;Goods&gt; page = goodsMapper.findByPage(goods);\n\n        return new PageBean(page.getTotal(), page.getResult());\n    }\n</code></pre>\n<h2 id=\"h2-u5B9Eu73B0u6587u4EF6u4E0Au4F20\"><a name=\"实现文件上传\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现文件上传</h2><p>这里涉及的无非就是SpringMVC的文件上传，详细的教程请参看我的博客：<a href=\"http://tycoding.cn/2018/05/31/Spring-6/\">SpringMVC实现文件上传和下载</a></p>\n<p>因为本项目中前端使用了ElementUI+Vue.JS技术，所以前端的文件上传和回显教程请看我的博客：<a href=\"http://tycoding.cn/2018/08/05/vue-7/\">SpringMVC+ElementUI实现图片上传和回显</a></p>\n<p>除了代码的编写，这里还要在application.yml中进行配置：</p>\n<pre><code class=\"lang-yaml\">spring:\n  servlet:\n    multipart:\n      max-file-size: 10Mb\n      max-request-size: 100Mb\n</code></pre>\n<p>这就相当于SpringMVC的XML配置：</p>\n<pre><code class=\"lang-xml\">&lt;bean id=&quot;multipartResolver&quot; class=&quot;org.springframework.web.multipart.commons.CommonsMultipartResolver&quot;&gt;\n        &lt;property name=&quot;maxUploadSize&quot; value=&quot;500000&quot;/&gt;\n&lt;/bean&gt;\n</code></pre>\n<h2 id=\"h2--spring-aop-\"><a name=\"使用Spring AOP切面编程实现简单的登录拦截器\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>使用Spring AOP切面编程实现简单的登录拦截器</h2><p>本项目，我们先不整合Shiro和Spring Security这些安全框架，使用Spring AOP切面编程思想实现简单的登录拦截：</p>\n<pre><code class=\"lang-java\">@Component\n@Aspect\npublic class MyInterceptor {\n\n    @Pointcut(&quot;within (cn.tycoding.controller..*) &amp;&amp; !within(cn.tycoding.controller.admin.LoginController)&quot;)\n    public void pointCut() {\n    }\n    @Around(&quot;pointCut()&quot;)\n    public Object trackInfo(ProceedingJoinPoint joinPoint) throws Throwable {\n        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n        HttpServletRequest request = attributes.getRequest();\n        User user = (User) request.getSession().getAttribute(&quot;user&quot;);\n        if (user == null) {\n            attributes.getResponse().sendRedirect(&quot;/login&quot;); //手动转发到/login映射路径\n        }\n        return joinPoint.proceed();\n    }\n}\n</code></pre>\n<p><strong>解释</strong></p>\n<p>关于Spring AOP的切面编程请自行百度，或者你也可以看我的博客：<a href=\"http://tycoding.cn/2018/05/25/Spring-3/\">Spring AOP思想</a>。我们需要注意以下几点</p>\n<ol>\n<li><p>一定要熟悉AspectJ的切点表达式，在这里：<code>..*</code>表示其目录下的所有方法和子目录方法。</p>\n</li><li><p>如果进行了登录拦截，即在session中没有获取到用户的登录信息，我们可能需要手动转发到<code>login</code>页面，这里访问的是<code>login</code>映射。</p>\n</li><li><p>基于2，一定要指定Object返回值，若AOP拦截的Controller return了一个视图地址，那么本来Controller应该跳转到这个视图地址的，但是被AOP拦截了，那么原来Controller仍会执行return，但是视图地址却找不到404了。</p>\n</li><li><p>切记一定要调用proceed()方法，proceed()：执行被通知的方法，如不调用将会阻止被通知的方法的调用，也就导致Controller中的return会404。</p>\n</li></ol>\n<h1 id=\"h1-preview\"><a name=\"Preview\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Preview</h1><p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-5ee5d4142c7df1c2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-ed364d2f838465c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-98635201a03eb4a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-0ca3c4c60e3abc54.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><br/></p>\n<h1 id=\"h1-u4EA4u6D41\"><a name=\"交流\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>交流</h1><p>如果大家有兴趣，欢迎大家加入我的Java交流技术群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！</p>\n<p><br/></p>\n<h1 id=\"h1-u8054u7CFB\"><a name=\"联系\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>联系</h1><p>If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.</p>\n<ul>\n<li><a href=\"http://www.tycoding.cn\">Blog<a href=\"https://my.oschina.net/u/3955926\"\">&#64;TyCoding</a>‘s blog</a></li><li><a href=\"https://github.com/TyCoding\">GitHub<a href=\"https://my.oschina.net/u/3955926\"\">&#64;TyCoding</a></a></li><li><a href=\"https://www.zhihu.com/people/tomo-83-82/activities\">ZhiHu<a href=\"https://my.oschina.net/u/3955926\"\">&#64;TyCoding</a></a></li></ul>\n', '继上篇文章：[Spring-Boot入门之环境搭建](http://tycoding.cn/2018/09/28/spring-boot/)。这次我们整合SpringBoot-Mybatis实现简单的CRUD业务。\n\n需求：\n\n* 详解SpringBoot工程的构建、与SSM项目在工程搭建上的不同。\n* 实现SpringBoot-Mybatis整合征服数据库。\n* 解决页面跳转，详解与SSM阶段的不同。\n* 实现分页查询，使用PaheHelper插件和ElementUI分页控件。\n* 实现文件上传。\n* 使用Spring AOP切面编程实现简易的实现登录拦截工程。\n\n**项目源码**请看我的Github仓库：[教你优雅的入门Spring Boot框架](https://github.com/TyCoding/spring-boot)\n\n**如果觉得不错就点击右上角star鼓励一下笔者吧(#^.^#)**\n\n\n# 教你优雅的入门Spring Boot框架\n\n**技术栈**\n\n* 后端： SpringBoot + Mybatis\n* 前端： Vue.JS + ElementUI\n\n**测试环境**\n\n* IDEA + SpringBoot-2.0.5\n\n**项目设计**\n\n```\n.\n├── db  -- sql文件\n├── mvnw \n├── mvnw.cmd\n├── pom.xml  -- 项目依赖\n└── src\n    ├── main\n    │   ├── java\n    │   │   └── cn\n    │   │       └── tycoding\n    │   │           ├── SpringbootApplication.java  -- Spring Boot启动类\n    │   │           ├── controller  -- MVC-WEB层\n    │   │           ├── entity  -- 实体类\n    │   │           ├── interceptor  -- 自定义拦截器\n    │   │           ├── mapper  -- mybatis-Mapper层接口\n    │   │           └── service  -- service业务层\n    │   └── resources  -- Spring Boot资源文件 \n    │       ├── application.yml  -- Spring Boot核心配置文件\n    │       ├── mapper  -- Mybatis Mapper层配置文件\n    │       ├── static  -- 前端静态文件\n    │       └── templates  -- Thymeleaf模板引擎识别的HTML页面目录\n    └── test  -- 测试文件\n```\n\n# 准备\n\n开始实战Spring Boot项目，首先，你需要将Spring Boot工程搭建出来。\n\nSpring Boot工程搭建请看我的博客：[Spring Boot入门之工程搭建](http://tycoding.cn/2018/09/28/spring-boot/)\n\n## Spring Boot应用启动器\n\nSpring Boot提供了很多应用启动器，分别用来支持不同的功能，说白了就是`pom.xml`中的依赖配置，因为Spring Boot的自动化配置特性，我们并不需再考虑项目依赖版本问题，使用Spring Boot的应用启动器，它能自动帮我们将相关的依赖全部导入到项目中。\n\n我们这里介绍几个常见的应用启动器：\n\n* `spring-boot-starter`: Spring Boot的核心启动器，包含了自动配置、日志和YAML\n* `spring-boot-starter-aop`: 支持AOP面向切面编程的功能，包括spring-aop和AspecJ\n* `spring-boot-starter-cache`: 支持Spring的Cache抽象\n* `spring-boot-starter-artermis`: 通过Apache Artemis支持JMS（Java Message Service）的API\n* `spring-boot-starter-data-jpa`: 支持JPA\n* `spring-boot-starter-data-solr`: 支持Apache Solr搜索平台，包括spring-data-solr\n* `spring-boot-starter-freemarker`: 支持FreeMarker模板引擎\n* `spring-boot-starter-jdbc`: 支持JDBC数据库\n* `spring-boot-starter-Redis`: 支持Redis键值储存数据库，包括spring-redis\n* `spring-boot-starter-security`: 支持spring-security\n* `spring-boot-starter-thymeleaf`: 支持Thymeleaf模板引擎，包括与Spring的集成\n* `spring-boot-starter-web`: 支持全栈式web开发，包括tomcat和Spring-WebMVC\n* `spring-boot-starter-log4j`: 支持Log4J日志框架\n* `spring-boot-starter-logging`: 引入Spring Boot默认的日志框架Logback\n\n## Spring Boot项目结构设计\n\nSpring Boot项目（即Maven项目），当然拥有最基础的Maven项目结构。除此之外：\n\n1. Spring Boot项目中不包含webapp(webroot)目录。\n2. Spring Boot默认提供的静态资源目录需要置于classpath下，且其下的目录名称要符合一定规定。\n3. Spring Boot默认不提倡用XML配置文件，主张使用YML作为配置文件格式，YML有更简洁的语法。当然也可以使用.properties作为配置文件格式。\n4. Spring Boot官方推荐使用Thymeleaf作为前端模板引擎，并且Thymeleaf默认将templates作为静态页面的存放目录（由配置文件指定）。\n5. Spring Boot默认将`resources`作为静态资源的存放目录，存放前端静态文件、项目配置文件。\n\n6. Spring Boot规定`resources`下的子级目录名要符合一定规则，一般我们设置`resources/static`为前端静态（JS,CSS）的存放目录；设置`resources/templates`作为HTML页面的存放目录。\n\n7. Spring Boot指定的Thymeleaf模板引擎文件目录`/resources/templates`是受保护的目录，想当与之前的WEB-INF文件夹，里面的静态资源不能直接访问，一般我们通过Controller映射访问。\n\n8. 建议将Mybatis-Mapper的XML映射文件放于`resources/`目录下，我这里设为`resources/mapper`目录，且`src/main/java/Mapper`下的Mapper层接口要使用`@Mapper`注解标识，不然mybatis找不到接口对应的XML映射文件。\n\n9. `SpringBootApplication.java`为项目的启动器类，项目不需要部署到Tomcat上，由SpringBoot提供的服务器部署项目（运行启动器类即可）；且SpringBoot会自动扫描该启动器同级和子级下用注解标识的Bean。\n\n10. Spring Boot不建议使用JSP页面，如果想使用，请自行百度解决办法。\n\n11. 上面说了Spring Boot提供的存放HTML静态页面的目录`resources/templates`是受保护的目录，访问其中的HTML页面要通过Controller映射，这就间接规定了你需要配置Spring的视图解析器，且Controller类不能使用`@RestController`标识。\n\n\n# 起步\n\n*首先：*  **我想特殊强调的是：SpringBoot不是对Spring功能上的增强，而是提供了一种快速使用Spring的方式**。一定要切记这一点。\n\n学习SpringBoot框架，只是为了更简便的使用Spring框架，我们在SSM阶段学习的知识现在放在Spring Boot框架上开发是完全适用的，我们学习的大多数是SpringBoot的自动化配置方式。\n\n因为Spring Boot框架的一大优势就是自动化配置，从pom.xml的配置中就能明显感受到。\n\n所以这里推荐一下我之前的SSM阶段整合项目： [SSM详细入门整合案例](https://github.com/TyCoding/ssm)    [SSM+Redis+Shiro+Solr+Vue.js整合项目](https://github.com/TyCoding/ssm-redis-solr)\n\n## 项目依赖\n\n本项目的依赖文件请看Github仓库：[spring-boot/pom.xml](https://github.com/TyCoding/spring-boot/blob/master/pom.xml)\n\n## 初始化数据库\n\n本项目数据库表设计请看GitHub仓库：[spring-boot/db/](https://github.com/TyCoding/spring-boot/tree/master/db)\n\n请运行项目前，先把数据库表结构建好\n\n## SpringBoot整合Mybatis\n\n之前已经说过：**SpringBoot框架不是对Spring功能上的增强，而是提供了一种快速使用Spring的方式**\n\n所以说，SpringBoot整合Mybatis的思想和Spring整合Mybatis的思想基本相同，不同之处有两点：\n\n* 1.Mapper接口的XML配置文件变化。之前我们使用Mybatis接口代理开发，规定Mapper映射文件要和接口在一个目录下；而这里Mapper映射文件置于`resources/mapper/`下，且置于`src/main/java/`下的Mapper接口需要用`@Mapper`注解标识，不然映射文件与接口无法匹配。\n\n* 2.SpringBoot建议使用YAML作为配置文件，它有更简便的配置方式。所以整合Mybatis在配置文件上有一定的区别，但最终都是那几个参数的配置。\n\n关于YAML的语法请自行百度，我这里也仅仅是满足基本的配置需求，不涉及那种不易理解的语法。\n\n### 整合配置文件\n\n本例详细代码请看GitHub仓库：[spring-boot/resources/application.yml](https://github.com/TyCoding/spring-boot/blob/master/src/main/resources/application.yml)\n\n在Spring阶段用XML配置mybatis无非就是配置：1.连接池；2.数据库url连接；3.mysql驱动；4.其他初始化配置\n\n```YAML\nspring:\n  datasource:\n    name: springboot\n    type: com.alibaba.druid.pool.DruidDataSource\n    #druid相关配置\n    druid:\n      #监控统计拦截的filters\n      filter: stat\n      #mysql驱动\n      driver-class-name: com.mysql.jdbc.Driver\n      #基本属性\n      url: jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true\n      username: root\n      password: root\n      #配置初始化大小/最小/最大\n      initial-size: 1\n      min-idle: 1\n      max-active: 20\n      #获取连接等待超时时间\n      max-wait: 60000\n      #间隔多久进行一次检测，检测需要关闭的空闲连接\n      time-between-eviction-runs-millis: 60000\n\n  #mybatis配置\n  mybatis:\n    mapper-locations: classpath:mapper/*.xml\n    type-aliases-package: cn.tycoding.entity\n```\n\n**注意：空格代表节点层次；注释部分用`#`标记**\n\n**解释**\n\n1. 我们实现的是spring-mybatis的整合，包含mybatis的配置以及datasource数据源的配置当然属于spring配置中的一部分，所以需要在`spring:`下。\n\n2. `mapper-locations`相当于XML中的`<property name=\"mapperLocations\">`用来扫描Mapper层的配置文件，由于我们的配置文件在`resources`下，所以需要指定`classpath:`。\n\n3. `type-aliases-package`相当与XML中`<property name=\"typeAliasesPackase\">`别名配置，一般取其下实体类类名作为别名。\n\n4. `datasource`数据源的配置，`name`表示当前数据源的名称，类似于之前的`<bean id=\"dataSource\">`id属性，这里可以任意指定，因为我们无需关注Spring是怎么注入这个Bean对象的。\n\n5. `druid`代表本项目中使用了阿里的druid连接池，`driver-class-name:`相当于XML中的`<property name=\"driverClassName\">`；`url`代表XML中的`<property name=\"url\">`；`username`代表XML中的`<property name=\"username\">`；`password`代表XML中的`<property name=\"password\">`；其他druid的私有属性配置不再解释。这里注意druid连接池和c3p0连接池在XML的<property>的name中就不同，在此处SpringBoot的配置中当然名称也不同。\n\n\n如果Spring整合Mybtis的配置你已经很熟悉了，那么这个配置你肯定也很眼熟，从英文名称上就很容易区分出来。这里需要注意的就是YAML语法规定不同行空格代表了不同的层级结构。\n\n既然完成了SpringBoot-Mybatis基本配置下面我们实战讲解如何实现基本的CRUD。\n\n### 实现查询\n\n> 1.在`src/main/java/cn/tycoding/entity/`下新建`User.java`实体类\n\n```java\npublic class User implements Serializable {\n    private Long id; //编号\n    private String username; //用户名\n    private String password; //密码\n    //getter/setter\n}\n```\n\n> 2.在`src/main/java/cn/tycoding/service/`下创建`BaseService.java`通用接口，目的是简化service层接口基本CRUD方法的编写。\n\n```java\npublic interface BaseService<T> {\n\n    // 查询所有\n    List<T> findAll();\n\n    //根据ID查询\n    List<T> findById(Long id);\n\n    //添加\n    void create(T t);\n\n    //删除（批量）\n    void delete(Long... ids);\n\n    //修改\n    void update(T t);\n}\n```\n\n以上就是我对Service层基本CRUD接口的简易封装，使用了泛型类，其继承接口指定了什么泛型，T就代表什么类。\n\n> 3.在`src/main/java/cn/tycoding/service/`下创建`UserService.java`接口：\n\n```java\npublic interface UserService extends BaseService<User> {}\n```\n\n> 4.在`src/main/java/cn/tycoding/service/impl/`下创建`UserServiceImpl.java`实现类：\n\n```java\n@Service\npublic class UserServiceImpl implements UserService {\n\n    @Autowired\n    private UserMapper userMapper;\n\n    @Override\n    public List<User> findAll() {\n        return userMapper.findAll();\n    }\n  \n    //其他方法省略\n}\n```\n\n> 5.在`src/main/java/cn/tycoding/mapper/`下创建`UserMapper.java`Mapper接口类：\n\n```java\n@Mapper\npublic interface UserMapper {\n    List<User> findAll();\n}\n```\n\n如上，我们一定要使用`@Mapper`接口标识这个接口，不然Mybatis找不到其对应的XML映射文件。\n\n> 6.在`src/main/resources/mapper/`下创建`UserMapper.xml`映射文件：\n\n```xml\n<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\" >\n<mapper namespace=\"cn.tycoding.mapper.UserMapper\">\n\n    <!-- 查询所有 -->\n    <select id=\"findAll\" resultType=\"cn.tycoding.entity.User\">\n        SELECT * FROM tb_user\n    </select>\n</mapper>\n```\n\n> 7.在`src/main/java/cn/tycoding/controller/admin/`下创建`UserController.java`\n\n```java\n@RestController\npublic class UserController {\n    @Autowired\n    private UserService userService;\n    \n    @RequestMapping(\"/findAll\")\n    public List<User> findAll() {\n        return userService.findAll();\n    }\n}\n```\n\n> 8.运行`src/main/java/cn/tycoding/SpringbootApplication.java`的main方法，启动springboot\n\n在浏览器上访问`localhost:8080/findAll`即可得到一串JSON数据。\n\n\n### 思考\n\n看了上面一步步的讲解。你应该明白了，其实和SSM阶段的CRUD基本相同，这里我就不再举例其他方法。\n\n下面我们讲解一下不同的地方：\n\n## 实现页面跳转\n\n因为Thymeleaf指定的目录`src/main/resources/templates/`是受保护的目录，其下的资源不能直接通过浏览器访问，可以使用Controller映射的方式访问，怎么映射呢？\n\n> 1.在application.yml中添加配置\n\n```yaml\nspring:\n  thymeleaf:\n    prefix: classpath:/templates/\n    check-template-location: true\n    suffix: .html\n    encoding: UTF-8\n    mode: LEGACYHTML5\n    cache: false\n```\n\n指定Thymeleaf模板引擎扫描`resources`下的`templates`文件夹中已`.html`结尾的文件。这样就实现了MVC中关于视图解析器的配置：\n\n```xml\n    <!-- 配置视图解析器 -->\n    <bean class=\"org.springframework.web.servlet.view.InternalResourceViewResolver\">\n        <property name=\"prefix\" value=\"/\"/>\n        <property name=\"suffix\" value=\".jsp\"/>\n    </bean>\n```\n\n是不是感觉方便很多呢？但这里需要注意的是：`classpath:`后的目录地址一定要先加`/`，比如目前的`classpath:/templates/`。\n\n> 2.在Controller添加映射方法\n\n```java\n    @GetMapping(value = {\"/\", \"/index\"})\n    public String index() {\n        return \"home/index\";\n    }\n```\n\n这样，访问`localhost:8080/index`将直接跳转到`resources/templates/home/index.html`页面。\n\n\n## 实现分页查询\n\n首先我们需要在application.yml中配置pageHelper插件\n\n```yaml\npagehelper:\n  pagehelperDialect: mysql\n  reasonable: true\n  support-methods-arguments: true\n```\n\n我这里使用了Mybatis的PageHelper分页插件，前端使用了ElementUI自带的分页插件：具体的教程请查看我的博客：[SpringMVC+ElementUI实现分页查询](http://tycoding.cn/2018/07/30/vue-6/)\n\n**核心配置：**\n\n`UserServiceImp.java`\n\n```java\n    public PageBean findByPage(Goods goods, int pageCode, int pageSize) {\n        //使用Mybatis分页插件\n        PageHelper.startPage(pageCode, pageSize);\n\n        //调用分页查询方法，其实就是查询所有数据，mybatis自动帮我们进行分页计算\n        Page<Goods> page = goodsMapper.findByPage(goods);\n\n        return new PageBean(page.getTotal(), page.getResult());\n    }\n```\n\n## 实现文件上传\n\n这里涉及的无非就是SpringMVC的文件上传，详细的教程请参看我的博客：[SpringMVC实现文件上传和下载](http://tycoding.cn/2018/05/31/Spring-6/)\n\n因为本项目中前端使用了ElementUI+Vue.JS技术，所以前端的文件上传和回显教程请看我的博客：[SpringMVC+ElementUI实现图片上传和回显](http://tycoding.cn/2018/08/05/vue-7/)\n\n除了代码的编写，这里还要在application.yml中进行配置：\n\n```yaml\nspring:\n  servlet:\n    multipart:\n      max-file-size: 10Mb\n      max-request-size: 100Mb\n```\n\n这就相当于SpringMVC的XML配置：\n\n```xml\n<bean id=\"multipartResolver\" class=\"org.springframework.web.multipart.commons.CommonsMultipartResolver\">\n        <property name=\"maxUploadSize\" value=\"500000\"/>\n</bean>\n```\n\n## 使用Spring AOP切面编程实现简单的登录拦截器\n\n本项目，我们先不整合Shiro和Spring Security这些安全框架，使用Spring AOP切面编程思想实现简单的登录拦截：\n\n```java\n@Component\n@Aspect\npublic class MyInterceptor {\n\n    @Pointcut(\"within (cn.tycoding.controller..*) && !within(cn.tycoding.controller.admin.LoginController)\")\n    public void pointCut() {\n    }\n    @Around(\"pointCut()\")\n    public Object trackInfo(ProceedingJoinPoint joinPoint) throws Throwable {\n        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();\n        HttpServletRequest request = attributes.getRequest();\n        User user = (User) request.getSession().getAttribute(\"user\");\n        if (user == null) {\n            attributes.getResponse().sendRedirect(\"/login\"); //手动转发到/login映射路径\n        }\n        return joinPoint.proceed();\n    }\n}\n```\n\n**解释**\n\n关于Spring AOP的切面编程请自行百度，或者你也可以看我的博客：[Spring AOP思想](http://tycoding.cn/2018/05/25/Spring-3/)。我们需要注意以下几点\n\n1. 一定要熟悉AspectJ的切点表达式，在这里：`..*`表示其目录下的所有方法和子目录方法。\n\n2. 如果进行了登录拦截，即在session中没有获取到用户的登录信息，我们可能需要手动转发到`login`页面，这里访问的是`login`映射。\n\n3. 基于2，一定要指定Object返回值，若AOP拦截的Controller return了一个视图地址，那么本来Controller应该跳转到这个视图地址的，但是被AOP拦截了，那么原来Controller仍会执行return，但是视图地址却找不到404了。\n\n4. 切记一定要调用proceed()方法，proceed()：执行被通知的方法，如不调用将会阻止被通知的方法的调用，也就导致Controller中的return会404。\n\n\n# Preview\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-5ee5d4142c7df1c2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-ed364d2f838465c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-98635201a03eb4a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-0ca3c4c60e3abc54.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n\n\n<br/>\n\n# 交流\n\n如果大家有兴趣，欢迎大家加入我的Java交流技术群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！\n\n<br/>\n\n# 联系\n\nIf you have some questions after you see this article, you can contact me or you can find some info by clicking these links.\n\n- [Blog[@TyCoding](https://my.oschina.net/u/3955926)\'s blog](http://www.tycoding.cn)\n- [GitHub[@TyCoding](https://my.oschina.net/u/3955926)](https://github.com/TyCoding)\n- [ZhiHu[@TyCoding](https://my.oschina.net/u/3955926)](https://www.zhihu.com/people/tomo-83-82/activities)', NULL, 'http://tycoding.cn', '1', 1148, '2018-02-01 00:00:01', '2018-02-01 00:00:01', '2018-11-02 12:45:22', 0);
INSERT INTO `tb_article` VALUES (7, 'Vue+ElementUI+SpringMVC实现图片上传和回显', '/site/images/thumbs/9.jpg', '涂陌', '<p>Vue+ElementUI+SpringMVC实现图片上传和table回显</p>\n<p>在之前我们已经讲过了 <a href=\"http://tycoding.cn/2018/07/30/vue-6/#more\">Vue+ElementUI+SpringMVC实现分页</a> 。</p>\n<p>而我们也常遇到表单中包含图片上传的需求，并且需要在table中显示图片，所以这里我就讲一下结合后端的SpringMVC框架如何实现图片上传并提交到表单中，在table表格中回显照片。</p>\n<p>本案例对应的<strong>开源项目地址</strong>请看我的GitHub仓库：</p>\n<ul>\n<li><p><a href=\"https://github.com/TyCoding/spring-boot\">优雅的入门SpringBoot+Mybatis，实现简单的CRUD </a></p>\n</li><li><p><a href=\"https://github.com/TyCoding/ssm-redis-solr\">优雅的实现电商项目中搜索功能，整合SSM+Redis+Shiro+Solr框架，教你使用Vue+ElementUI写一个炫酷的后端页面 </a></p>\n</li></ul>\n<!--more-->\n<p><br/></p>\n<p><strong>写在前面</strong></p>\n<p>本篇博文主要讲Vue.js+ElementUI如何实现图片上传和提交表单，前端技术会讲多一点，因此：</p>\n<ul>\n<li>如果你对SpringMVC文件上传和下载不是很清楚，请查看我这篇博文： <a href=\"http://tycoding.cn/2018/05/31/Spring-6/#more\">SpringMVC实现文件上传和下载</a></li><li>因为案例基于SSM框架，如果你你对SSM框架不是很清楚，请查看我这篇博文：<a href=\"http://tycoding.cn/2018/06/05/ssm-2/#more\">SSM框架整合</a>  <a href=\"https://github.com/TyCoding/ssm\">GitHub</a></li></ul>\n<h1 id=\"h1-u51C6u5907\"><a name=\"准备\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>准备</h1><p><strong>首先</strong>，请一定阅读一下我的 <a href=\"http://tycoding.cn/2018/05/31/Spring-6/\">SpringMVC实现文件上传和下载</a> 本篇博文将不在详细讲这部分内容。</p>\n<p><strong>前端：</strong></p>\n<blockquote>\n<p>你会用到以下技术：</p>\n<p>Vue.js</p>\n<p>Vue-resource.js</p>\n<p>ElementUI</p>\n</blockquote>\n<p>我们将实现的效果是什么呢？</p>\n<p><em>图片上传：</em></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-e3db36f5a5fe0a4a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><em>table展示：</em></p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-017fa70e43233ecc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<h1 id=\"h1-u601Du8DEFu5206u6790\"><a name=\"思路分析\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>思路分析</h1><p>想要实现图片上传和table的回显，让我们先分析以下实现思路：</p>\n<h2 id=\"h2-u56FEu7247u4E0Au4F20u548Cu8868u5355u63D0u4EA4\"><a name=\"图片上传和表单提交\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>图片上传和表单提交</h2><p>那么你就要明白图片上传和表单提交是两个功能，其对应不同的接口，表单中并不是保存了这个图片，而仅仅是保存了储存图片的路径地址。我们需要分析以下几点：</p>\n<p><strong>1、图片如何上传，什么时候上传？</strong></p>\n<p>图片应该在点击upload上传组件的时候就触发了对应的事件，当选择了要上传的图片，点击确定的时候就请求了后端的接口保存了图片。也就是说你在浏览器中弹出的选择框中选择了要上传的图片，当你点击确定的一瞬间就已将图片保存到了服务器上；而再点击提交表单的时候，储存在表单中的图片数据仅仅是刚才上传的图片存储地址。</p>\n<p><strong>2、如何获取到已经上传的图片的储存地址？</strong></p>\n<p>因为在浏览器上传选择框被确定选择的瞬间已经请求了后端接口保存了图片，我们该怎么知道图片在哪里储存呢？</p>\n<ul>\n<li><strong>前端：</strong> 比如我们使用了ElementUI提供的上传组件，其就存在一个上传成功的回调函数：<code>on-success</code>，这个回调函数被触发的时间点就是图片成功上传后的瞬间，我们就是要在这个回调函数触发的时候获取到图片储存的地址。</li><li><strong>后端：</strong> 上面讲了获取地址，这个<strong>地址</strong>就是后端返回给前端的数据（JSON格式）。因为后端图片上传接口配置图片储存的地址，如果图片上传成功，就将图片储存的地址以JSON格式返回给前端。</li></ul>\n<p><strong>3、如何提交表单</strong></p>\n<p>说如何提交表单，这就显得很简单了，因为上面我们已经完成了：1、图片成功上传；2、获取到了图片在服务器上的储存地址。利用Vue的双向绑定思想，在图片成功上传的回调函数<code>on-success</code>中获取到后端返回的图片储存地址，将这个地址赋值给Vue实例<code>data(){}</code>中定义的表单对象。这样在提交表单的时候仅需要将这个表单对象发送给后端，保存到数据库就行了。</p>\n<h2 id=\"h2--table-\"><a name=\"图片在table的回显\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>图片在table的回显</h2><p>想要将图片回显到table表格中其实很简单，前提只要你在数据库中保存了正确的图片储存地址；在table表格中我们仅需要在<code>&lt;td&gt;</code>列中新定义一列<code>&lt;td&gt;&lt;img src=&quot;图片的地址&quot;/&gt;&lt;/td&gt;</code>即可完成图片回显。渲染table数据的时候循环给<code>&lt;img&gt;</code>中的<code>src</code>赋值数据库中保存的图片url即可。</p>\n<p><br/></p>\n<h1 id=\"h1-u540Eu7AEFu5B9Eu73B0\"><a name=\"后端实现\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>后端实现</h1><p><br/></p>\n<h2 id=\"h2-u56FEu7247u4E0Au4F20u63A5u53E3\"><a name=\"图片上传接口\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>图片上传接口</h2><p><strong>注意：</strong> 关于SpringMVC如何实现文件上传和下载，请看我的博文： <a href=\"http://tycoding.cn/2018/05/31/Spring-6/\">SpringMVC实现文件上传和下载</a> 。这里我给出代码，就不再解释了(#^.^#)：</p>\n<p>这里我将文件上传和下载接口单独抽离在一个Controller类中：</p>\n<pre><code class=\"lang-java\">import com.instrument.entity.Result;\n\n@RestController\npublic class UploadDownController {\n\n    /**\n     * 文件上传\n     * @param picture\n     * @param request\n     * @return\n     */\n    @RequestMapping(&quot;/upload&quot;)\n    public Result upload(@RequestParam(&quot;picture&quot;) MultipartFile picture, HttpServletRequest request) {\n\n        //获取文件在服务器的储存位置\n        String path = request.getSession().getServletContext().getRealPath(&quot;/upload&quot;);\n        File filePath = new File(path);\n        System.out.println(&quot;文件的保存路径：&quot; + path);\n        if (!filePath.exists() &amp;&amp; !filePath.isDirectory()) {\n            System.out.println(&quot;目录不存在，创建目录:&quot; + filePath);\n            filePath.mkdir();\n        }\n\n        //获取原始文件名称(包含格式)\n        String originalFileName = picture.getOriginalFilename();\n        System.out.println(&quot;原始文件名称：&quot; + originalFileName);\n\n        //获取文件类型，以最后一个`.`为标识\n        String type = originalFileName.substring(originalFileName.lastIndexOf(&quot;.&quot;) + 1);\n        System.out.println(&quot;文件类型：&quot; + type);\n        //获取文件名称（不包含格式）\n        String name = originalFileName.substring(0, originalFileName.lastIndexOf(&quot;.&quot;));\n\n        //设置文件新名称: 当前时间+文件名称（不包含格式）\n        Date d = new Date();\n        SimpleDateFormat sdf = new SimpleDateFormat(&quot;yyyyMMddHHmmss&quot;);\n        String date = sdf.format(d);\n        String fileName = date + name + &quot;.&quot; + type;\n        System.out.println(&quot;新文件名称：&quot; + fileName);\n\n        //在指定路径下创建一个文件\n        File targetFile = new File(path, fileName);\n\n        //将文件保存到服务器指定位置\n        try {\n            picture.transferTo(targetFile);\n            System.out.println(&quot;上传成功&quot;);\n            //将文件在服务器的存储路径返回\n            return new Result(true,&quot;/upload/&quot; + fileName);\n        } catch (IOException e) {\n            System.out.println(&quot;上传失败&quot;);\n            e.printStackTrace();\n            return new Result(false, &quot;上传失败&quot;);\n        }\n    }\n}\n</code></pre>\n<p><strong>为什么返回一个Result数据类型？</strong></p>\n<p>注意这个<code>Result</code>是我自己声明的一个实体类，用于封装返回的结果信息，配合<code><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\">@RestController</a></code>注解实现将封装的信息以JSON格式return给前端，最后看下我定义的<code>Result</code>:</p>\n<pre><code class=\"lang-java\">public class Result implements Serializable {\n\n    //判断结果\n    private boolean success;\n    //返回信息\n    private String message;\n\n    public Result(boolean success, String message) {\n        this.success = success;\n        this.message = message;\n    }\n\n    public boolean isSuccess() {\n        return success;\n    }\n\n    setter/getter...\n}\n</code></pre>\n<h2 id=\"h2-u8868u5355u63D0u4EA4u63A5u53E3\"><a name=\"表单提交接口\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>表单提交接口</h2><p>表单提交大家都比较熟悉了，配合图片上传，仅仅是在实体类中多了一个字段存放图片的URL地址：</p>\n<pre><code class=\"lang-java\">@RestController\n@RequestMapping(&quot;/instrument&quot;)\npublic class InstrumentController {\n\n    //注入\n    @Autowired\n    private InstrumentService instrumentService;\n\n    /**\n     * 添加\n     *\n     * @param instrument\n     * @return\n     */\n    @RequestMapping(&quot;/save&quot;)\n    public Result save(Instrument instrument) {\n        if(instrument != null){\n            try{\n                instrumentService.save(instrument);\n                return new Result(true,&quot;添加成功&quot;);\n            }catch (Exception e){\n                e.printStackTrace();\n            }\n        }\n        return new Result(false, &quot;发生未知错误&quot;);\n    }\n}\n</code></pre>\n<p><strong>如上</strong></p>\n<p>大家可能会疑惑这个为什么返回Result类型的数据？ 答：为了前端方便判断接口执行成功与否。因为我前端使用的是<strong>HTML页面</strong>，想要从后端域对象中取数据显然就有点不现实了。</p>\n<p>我写Controller的时候定义了全局的<code><a href=\"https://github.com/RestController\" title=\"&#64;RestController\" class=\"at-link\">@RestController</a></code>注解，和<code><a href=\"https://github.com/Controller\" title=\"&#64;Controller\" class=\"at-link\">@Controller</a></code>注解的区别是，前者多了<code><a href=\"https://github.com/ResponseBody\" title=\"&#64;ResponseBody\" class=\"at-link\">@ResponseBody</a></code>注解，这样整合Controller类返回的数据都将给自动转换成JSON格式。</p>\n<p><br/></p>\n<h1 id=\"h1-u524Du7AEFu5B9Eu73B0\"><a name=\"前端实现\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>前端实现</h1><p><br/></p>\n<h2 id=\"h2-u5B9Eu73B0u56FEu7247u4E0Au4F20\"><a name=\"实现图片上传\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现图片上传</h2><p>这里我使用了ElementUI的文件上传组件： <a href=\"http://element-cn.eleme.io/#/zh-CN/component/upload\">官方文档</a> </p>\n<p>配合ElementUI的上传组件，我们会这样定义(这是form表单中的一部分)：</p>\n<pre><code class=\"lang-html\">&lt;el-form-item label=&quot;图片&quot;&gt;\n    &lt;el-upload\n               ref=&quot;upload&quot;\n               action=&quot;/upload.do&quot;\n               name=&quot;picture&quot;\n               list-type=&quot;picture-card&quot;\n               :limit=&quot;1&quot;\n               :file-list=&quot;fileList&quot;\n               :on-exceed=&quot;onExceed&quot;\n               :before-upload=&quot;beforeUpload&quot;\n               :on-preview=&quot;handlePreview&quot;\n               :on-success=&quot;handleSuccess&quot;\n               :on-remove=&quot;handleRemove&quot;&gt;\n        &lt;i class=&quot;el-icon-plus&quot;&gt;&lt;/i&gt;\n    &lt;/el-upload&gt;\n    &lt;el-dialog :visible.sync=&quot;dialogVisible&quot;&gt;\n        &lt;img width=&quot;100%&quot; :src=&quot;dialogImageUrl&quot; alt=&quot;&quot;&gt;\n    &lt;/el-dialog&gt;\n&lt;/el-form-item&gt;\n</code></pre>\n<p>注意，我这里仅展示了文件上传的<code>form-item</code>，ElementUI的表单声明是：<code>&lt;el-form&gt;</code> <strong>注意</strong> 表单中不需要指定<code>enctype=&quot;multipart/form-data&quot;</code>这个参数，与我们普通的文件上传表单是不同的。</p>\n<p>了解几个参数：</p>\n<ul>\n<li><strong>ref</strong> <code>ref</code>是Vue原生参数，用来给组件注册引用信息。引用信息将会注册到父组件的<code>$refs</code>对象上，如果定义在普通的DOM元素上，那么<code>$refs</code>指向的就是DOM元素。</li></ul>\n<ul>\n<li><strong>action</strong> <code>action</code>表示此上传组件对应的上传接口，此时我们使用的是后端Controller定义的接口</li></ul>\n<ul>\n<li><strong>name</strong> <code>name</code>表示当前组件上传的文件字段名，需要和后端的上传接口字段名相同 。</li></ul>\n<ul>\n<li><strong>list-type</strong> 文件列表的类型，主要是文件列表的样式定义。这里是卡片化。</li></ul>\n<ul>\n<li><strong>:limit</strong> 最大允许上传的文件个数。</li></ul>\n<ul>\n<li><strong>file-list</strong> 上传的文件列表，这个参数用于在这个上传组件中回显图片，包含两个参数：<code>name、url</code>如果你想在这个文件上传组件中咱叔图片，赋值对应的参数即可显示，比如更新数据时，其表单样式完全和添加表单是相同的。但是table中回显图片是完全不需要用这个方式的。</li></ul>\n<ul>\n<li><strong>:on-exceed</strong> 上传文件超出个数时的钩子函数。</li></ul>\n<ul>\n<li><strong>:before-upload</strong> 上传文件前的钩子函数，参数为上传的文件，返回false，就停止上传。</li></ul>\n<ul>\n<li><strong>:on-preview</strong> 点击文件列表中已上传的文件时的钩子函数</li></ul>\n<ul>\n<li><strong>:on-success</strong> 文件上传成功的钩子函数</li></ul>\n<ul>\n<li><strong>:on-remove</strong> 文件列表移除时的钩子函数</li></ul>\n<ul>\n<li><strong>:src</strong> 图片上传的URL。</li></ul>\n<p><strong>JS部分</strong></p>\n<pre><code class=\"lang-javascript\">//设置全局表单提交格式\nVue.http.options.emulateJSON = true;\n\nnew Vue({\n    el: &#39;#app&#39;,\n    data(){\n        return{\n            //文件上传的参数\n            dialogImageUrl: &#39;&#39;,\n            dialogVisible: false,\n            //图片列表（用于在上传组件中回显图片）\n            fileList: [{name: &#39;&#39;, url: &#39;&#39;}],\n        }\n    },\n    methods(){\n           //文件上传成功的钩子函数\n        handleSuccess(res, file) {\n            this.$message({\n                type: &#39;info&#39;,\n                message: &#39;图片上传成功&#39;,\n                duration: 6000\n            });\n            if (file.response.success) {\n                this.editor.picture = file.response.message; //将返回的文件储存路径赋值picture字段\n            }\n        },\n        //删除文件之前的钩子函数\n        handleRemove(file, fileList) {\n            this.$message({\n                type: &#39;info&#39;,\n                message: &#39;已删除原有图片&#39;,\n                duration: 6000\n            });\n        },\n        //点击列表中已上传的文件事的钩子函数\n        handlePreview(file) {\n        },\n        //上传的文件个数超出设定时触发的函数\n        onExceed(files, fileList) {\n            this.$message({\n                type: &#39;info&#39;,\n                message: &#39;最多只能上传一个图片&#39;,\n                duration: 6000\n            });\n        },\n        //文件上传前的前的钩子函数\n        //参数是上传的文件，若返回false，或返回Primary且被reject，则停止上传\n        beforeUpload(file) {\n            const isJPG = file.type === &#39;image/jpeg&#39;;\n            const isGIF = file.type === &#39;image/gif&#39;;\n            const isPNG = file.type === &#39;image/png&#39;;\n            const isBMP = file.type === &#39;image/bmp&#39;;\n            const isLt2M = file.size / 1024 / 1024 &lt; 2;\n\n            if (!isJPG &amp;&amp; !isGIF &amp;&amp; !isPNG &amp;&amp; !isBMP) {\n                this.$message.error(&#39;上传图片必须是JPG/GIF/PNG/BMP 格式!&#39;);\n            }\n            if (!isLt2M) {\n                this.$message.error(&#39;上传图片大小不能超过 2MB!&#39;);\n            }\n            return (isJPG || isBMP || isGIF || isPNG) &amp;&amp; isLt2M;\n        },     \n    }\n});\n</code></pre>\n<p><strong>解释</strong></p>\n<p>如上的JS代码，主要是定义一些钩子函数，这里我么里梳理一下逻辑：</p>\n<p>1、点击ElementUI的上传组件，浏览器自动弹出文件上传选择窗口，我们选择要上传的图片。</p>\n<p>2、选择好了要上传的图片，点击弹窗右下角的确定按钮触发JS中定义的钩子函数。</p>\n<p>3、首先触发的钩子函数是<code>beforeUpload(file)</code>函数，其中的参数<code>file</code>即代表当前上传的文件对象，<code>beforeUpload()</code>定义了对上传文件格式校验。如果不是允许的格式就弹出错误信息，并阻止文件上传，若我那件格式允许，则继续执行。</p>\n<p>4、通过了<code>beforeUpload()</code>函数的校验，文件开始调用后端接口将数据发送给后端。文件的字段名：<code>picture</code>，格式：<code>multipart/form-data</code>，虽然我们的表单没有定义<code>enctype=&quot;multipart/form-data&quot;</code>属性，但是HTTP请求头会自动设置为<code>multipart/form-data</code>类型。</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-dcd28bddc31a63de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>5、这时，如果后端逻辑没有错误，已经正常的将图片上传到服务器上了，可以在指定文件夹中查看到已上传的图片，那么此时JS中会自动调用<code>handleSuccess()</code>钩子函数，因为我们设置后端上传接口上传成功返回的数据是文件的保存路径：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-fce7f2c140ed4474.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>那我们就将这个路径通过Vue的双向绑定，赋值给表单对象的字段<code>picture</code>，那么提交表单的时候，该字段对应的值就是这个路径了。</p>\n<p>6、如果我们再点击上传文件按钮，就会触发<code>onExceed()</code>函数，因为我们设置的<code>limit</code>最多上传一个。</p>\n<p>7、如果点击图片中的删除按钮，就会触发<code>handleRemove()</code>函数，并删除此图片。</p>\n<p>8、如果点击了已上传的文件列表，就会触发<code>handlePreview()</code>函数。</p>\n<h2 id=\"h2-u5B9Eu73B0u8868u5355u63D0u4EA4\"><a name=\"实现表单提交\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现表单提交</h2><p>表单提交就比较简单了，就是触发对应的click事件，触发其中定义的函数，将已在<code>data(){}</code>中定义的表单数据发送给后端接口：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-05466c45309a29e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p>提交数据：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-3d038bd0378c8c57.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><strong>后端接口</strong></p>\n<pre><code class=\"lang-java\">@RequestMapping(&quot;/save&quot;)\npublic Result save(Instrument instrument) {\n    if(instrument != null){\n        try{\n            instrumentService.save(instrument);\n            return new Result(true,&quot;添加成功&quot;);\n        }catch (Exception e){\n            e.printStackTrace();\n        }\n    }\n    return new Result(false, &quot;发生未知错误&quot;);\n}\n</code></pre>\n<p>数据库中保存的数据：</p>\n<p><img src=\"http://upload-images.jianshu.io/upload_images/12613204-31c0ab4e6f42d8db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240\" alt=\"image\"></p>\n<p><br/></p>\n<h1 id=\"h1--table-\"><a name=\"实现table回显图片\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>实现table回显图片</h1><p>table回显图片也是很简单的，仅需要在列中增加一列：</p>\n<pre><code class=\"lang-html\">&lt;el-table :data=&quot;instrument&quot;&gt;\n    &lt;el-table-column label=&quot;图片&quot; width=&quot;130&quot;&gt;\n        &lt;template scope=&quot;scope&quot;&gt;\n            &lt;img :src=&quot;scope.row.picture&quot; class=&quot;picture&quot;/&gt;\n        &lt;/template&gt;\n    &lt;/el-table-column&gt;\n    &lt;el-table-column\n         label=&quot;运行状态&quot;\n         width=&quot;80&quot;\n         prop=&quot;operatingStatus&quot;&gt;\n    &lt;/el-table-column&gt;\n&lt;/el-table&gt;\n</code></pre>\n<p>因为使用Vue，根据其双向绑定的思想，再结合Element-UI提供渲染表格的方式是在<code>&lt;el-table&gt;</code>的<code>:data</code>中指定对应要渲染的数据即可。</p>\n<p><strong>注意</strong> ElementUI渲染table的方式是：1、<code>&lt;el-table&gt;</code>中定义<code>:data</code>；2、<code>&lt;el-table-column&gt;</code>中定义<code>prop=&quot;data中的参数&quot;</code>。但是因为我们要显示的是图片而不是文本数据，所以要在<code>&lt;img&gt;</code>中定义<code>:src=&quot;data中的变量&quot;</code>即可实现渲染。</p>\n<p><br/></p>\n<p><strong>后端</strong>就是正常的查询数据库数据即可了，为什么数据库中保存了这个URL图片就能直接显示到HTML中，请看我这篇博文： <a href=\"http://tycoding.cn/2018/05/31/Spring-6/\">SpringMVC实现文件上传和下载</a> </p>\n<p><br/></p>\n<h1 id=\"h1-u4EA4u6D41\"><a name=\"交流\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>交流</h1><p>如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！</p>\n<p><br/></p>\n<h1 id=\"h1-u8054u7CFB\"><a name=\"联系\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>联系</h1><p>If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.</p>\n<ul>\n<li><a href=\"http://www.tycoding.cn\"\">Blog&#64;TyCoding’s blog</a></li><li><a href=\"https://github.com/TyCoding\"\">GitHub&#64;TyCoding</a></li><li><a href=\"https://www.zhihu.com/people/tomo-83-82/activities\"\">ZhiHu&#64;TyCoding</a></li></ul>\n', '\n\nVue+ElementUI+SpringMVC实现图片上传和table回显\n\n在之前我们已经讲过了 [Vue+ElementUI+SpringMVC实现分页](http://tycoding.cn/2018/07/30/vue-6/#more) 。\n\n而我们也常遇到表单中包含图片上传的需求，并且需要在table中显示图片，所以这里我就讲一下结合后端的SpringMVC框架如何实现图片上传并提交到表单中，在table表格中回显照片。\n\n\n本案例对应的**开源项目地址**请看我的GitHub仓库：\n\n* [优雅的入门SpringBoot+Mybatis，实现简单的CRUD ](https://github.com/TyCoding/spring-boot)\n\n* [优雅的实现电商项目中搜索功能，整合SSM+Redis+Shiro+Solr框架，教你使用Vue+ElementUI写一个炫酷的后端页面 ](https://github.com/TyCoding/ssm-redis-solr)\n\n<!--more-->\n\n<br/>\n\n**写在前面**\n\n本篇博文主要讲Vue.js+ElementUI如何实现图片上传和提交表单，前端技术会讲多一点，因此：\n\n* 如果你对SpringMVC文件上传和下载不是很清楚，请查看我这篇博文： [SpringMVC实现文件上传和下载](http://tycoding.cn/2018/05/31/Spring-6/#more)\n* 因为案例基于SSM框架，如果你你对SSM框架不是很清楚，请查看我这篇博文：[SSM框架整合](http://tycoding.cn/2018/06/05/ssm-2/#more)  [GitHub](https://github.com/TyCoding/ssm)\n\n\n\n# 准备\n\n**首先**，请一定阅读一下我的 [SpringMVC实现文件上传和下载](http://tycoding.cn/2018/05/31/Spring-6/) 本篇博文将不在详细讲这部分内容。\n\n**前端：**\n\n\n\n> 你会用到以下技术：\n>\n> Vue.js\n>\n> Vue-resource.js\n>\n> ElementUI\n\n\n\n我们将实现的效果是什么呢？\n\n*图片上传：*\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-e3db36f5a5fe0a4a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n*table展示：*\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-017fa70e43233ecc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n# 思路分析\n\n想要实现图片上传和table的回显，让我们先分析以下实现思路：\n\n## 图片上传和表单提交\n\n那么你就要明白图片上传和表单提交是两个功能，其对应不同的接口，表单中并不是保存了这个图片，而仅仅是保存了储存图片的路径地址。我们需要分析以下几点：\n\n\n\n**1、图片如何上传，什么时候上传？**\n\n图片应该在点击upload上传组件的时候就触发了对应的事件，当选择了要上传的图片，点击确定的时候就请求了后端的接口保存了图片。也就是说你在浏览器中弹出的选择框中选择了要上传的图片，当你点击确定的一瞬间就已将图片保存到了服务器上；而再点击提交表单的时候，储存在表单中的图片数据仅仅是刚才上传的图片存储地址。\n\n\n\n**2、如何获取到已经上传的图片的储存地址？**\n\n因为在浏览器上传选择框被确定选择的瞬间已经请求了后端接口保存了图片，我们该怎么知道图片在哪里储存呢？\n\n* **前端：** 比如我们使用了ElementUI提供的上传组件，其就存在一个上传成功的回调函数：`on-success`，这个回调函数被触发的时间点就是图片成功上传后的瞬间，我们就是要在这个回调函数触发的时候获取到图片储存的地址。\n* **后端：** 上面讲了获取地址，这个**地址**就是后端返回给前端的数据（JSON格式）。因为后端图片上传接口配置图片储存的地址，如果图片上传成功，就将图片储存的地址以JSON格式返回给前端。\n\n\n\n**3、如何提交表单**\n\n说如何提交表单，这就显得很简单了，因为上面我们已经完成了：1、图片成功上传；2、获取到了图片在服务器上的储存地址。利用Vue的双向绑定思想，在图片成功上传的回调函数`on-success`中获取到后端返回的图片储存地址，将这个地址赋值给Vue实例`data(){}`中定义的表单对象。这样在提交表单的时候仅需要将这个表单对象发送给后端，保存到数据库就行了。\n\n\n\n## 图片在table的回显\n\n想要将图片回显到table表格中其实很简单，前提只要你在数据库中保存了正确的图片储存地址；在table表格中我们仅需要在`<td>`列中新定义一列`<td><img src=\"图片的地址\"/></td>`即可完成图片回显。渲染table数据的时候循环给`<img>`中的`src`赋值数据库中保存的图片url即可。\n\n<br/>\n\n# 后端实现\n\n<br/>\n\n## 图片上传接口\n\n**注意：** 关于SpringMVC如何实现文件上传和下载，请看我的博文： [SpringMVC实现文件上传和下载](http://tycoding.cn/2018/05/31/Spring-6/) 。这里我给出代码，就不再解释了(#^.^#)：\n\n这里我将文件上传和下载接口单独抽离在一个Controller类中：\n\n```java\nimport com.instrument.entity.Result;\n\n@RestController\npublic class UploadDownController {\n\n    /**\n     * 文件上传\n     * @param picture\n     * @param request\n     * @return\n     */\n    @RequestMapping(\"/upload\")\n    public Result upload(@RequestParam(\"picture\") MultipartFile picture, HttpServletRequest request) {\n\n        //获取文件在服务器的储存位置\n        String path = request.getSession().getServletContext().getRealPath(\"/upload\");\n        File filePath = new File(path);\n        System.out.println(\"文件的保存路径：\" + path);\n        if (!filePath.exists() && !filePath.isDirectory()) {\n            System.out.println(\"目录不存在，创建目录:\" + filePath);\n            filePath.mkdir();\n        }\n\n        //获取原始文件名称(包含格式)\n        String originalFileName = picture.getOriginalFilename();\n        System.out.println(\"原始文件名称：\" + originalFileName);\n\n        //获取文件类型，以最后一个`.`为标识\n        String type = originalFileName.substring(originalFileName.lastIndexOf(\".\") + 1);\n        System.out.println(\"文件类型：\" + type);\n        //获取文件名称（不包含格式）\n        String name = originalFileName.substring(0, originalFileName.lastIndexOf(\".\"));\n\n        //设置文件新名称: 当前时间+文件名称（不包含格式）\n        Date d = new Date();\n        SimpleDateFormat sdf = new SimpleDateFormat(\"yyyyMMddHHmmss\");\n        String date = sdf.format(d);\n        String fileName = date + name + \".\" + type;\n        System.out.println(\"新文件名称：\" + fileName);\n\n        //在指定路径下创建一个文件\n        File targetFile = new File(path, fileName);\n\n        //将文件保存到服务器指定位置\n        try {\n            picture.transferTo(targetFile);\n            System.out.println(\"上传成功\");\n            //将文件在服务器的存储路径返回\n            return new Result(true,\"/upload/\" + fileName);\n        } catch (IOException e) {\n            System.out.println(\"上传失败\");\n            e.printStackTrace();\n            return new Result(false, \"上传失败\");\n        }\n    }\n}\n```\n\n**为什么返回一个Result数据类型？**\n\n注意这个`Result`是我自己声明的一个实体类，用于封装返回的结果信息，配合`@RestController`注解实现将封装的信息以JSON格式return给前端，最后看下我定义的`Result`:\n\n```java\npublic class Result implements Serializable {\n\n    //判断结果\n    private boolean success;\n    //返回信息\n    private String message;\n\n    public Result(boolean success, String message) {\n        this.success = success;\n        this.message = message;\n    }\n    \n    public boolean isSuccess() {\n        return success;\n    }\n    \n    setter/getter...\n}\n```\n\n\n\n\n\n## 表单提交接口\n\n表单提交大家都比较熟悉了，配合图片上传，仅仅是在实体类中多了一个字段存放图片的URL地址：\n\n```java\n@RestController\n@RequestMapping(\"/instrument\")\npublic class InstrumentController {\n\n    //注入\n    @Autowired\n    private InstrumentService instrumentService;\n\n    /**\n     * 添加\n     *\n     * @param instrument\n     * @return\n     */\n    @RequestMapping(\"/save\")\n    public Result save(Instrument instrument) {\n        if(instrument != null){\n            try{\n                instrumentService.save(instrument);\n                return new Result(true,\"添加成功\");\n            }catch (Exception e){\n                e.printStackTrace();\n            }\n        }\n        return new Result(false, \"发生未知错误\");\n    }\n}\n```\n\n\n\n**如上**\n\n大家可能会疑惑这个为什么返回Result类型的数据？ 答：为了前端方便判断接口执行成功与否。因为我前端使用的是**HTML页面**，想要从后端域对象中取数据显然就有点不现实了。\n\n我写Controller的时候定义了全局的`@RestController`注解，和`@Controller`注解的区别是，前者多了`@ResponseBody`注解，这样整合Controller类返回的数据都将给自动转换成JSON格式。\n\n\n\n<br/>\n\n# 前端实现\n\n<br/>\n\n## 实现图片上传\n\n这里我使用了ElementUI的文件上传组件： [官方文档](http://element-cn.eleme.io/#/zh-CN/component/upload) \n\n配合ElementUI的上传组件，我们会这样定义(这是form表单中的一部分)：\n\n```html\n<el-form-item label=\"图片\">\n    <el-upload\n               ref=\"upload\"\n               action=\"/upload.do\"\n               name=\"picture\"\n               list-type=\"picture-card\"\n               :limit=\"1\"\n               :file-list=\"fileList\"\n               :on-exceed=\"onExceed\"\n               :before-upload=\"beforeUpload\"\n               :on-preview=\"handlePreview\"\n               :on-success=\"handleSuccess\"\n               :on-remove=\"handleRemove\">\n        <i class=\"el-icon-plus\"></i>\n    </el-upload>\n    <el-dialog :visible.sync=\"dialogVisible\">\n        <img width=\"100%\" :src=\"dialogImageUrl\" alt=\"\">\n    </el-dialog>\n</el-form-item>\n```\n\n注意，我这里仅展示了文件上传的`form-item`，ElementUI的表单声明是：`<el-form>` **注意** 表单中不需要指定`enctype=\"multipart/form-data\"`这个参数，与我们普通的文件上传表单是不同的。\n\n了解几个参数：\n\n* **ref** `ref`是Vue原生参数，用来给组件注册引用信息。引用信息将会注册到父组件的`$refs`对象上，如果定义在普通的DOM元素上，那么`$refs`指向的就是DOM元素。\n\n\n\n* **action** `action`表示此上传组件对应的上传接口，此时我们使用的是后端Controller定义的接口\n\n\n\n* **name** `name`表示当前组件上传的文件字段名，需要和后端的上传接口字段名相同 。\n\n\n\n* **list-type** 文件列表的类型，主要是文件列表的样式定义。这里是卡片化。\n\n\n\n* **:limit** 最大允许上传的文件个数。\n\n\n\n* **file-list** 上传的文件列表，这个参数用于在这个上传组件中回显图片，包含两个参数：`name、url`如果你想在这个文件上传组件中咱叔图片，赋值对应的参数即可显示，比如更新数据时，其表单样式完全和添加表单是相同的。但是table中回显图片是完全不需要用这个方式的。\n\n\n\n* **:on-exceed** 上传文件超出个数时的钩子函数。\n\n\n\n* **:before-upload** 上传文件前的钩子函数，参数为上传的文件，返回false，就停止上传。\n\n\n\n* **:on-preview** 点击文件列表中已上传的文件时的钩子函数\n\n\n\n* **:on-success** 文件上传成功的钩子函数\n\n\n\n* **:on-remove** 文件列表移除时的钩子函数\n\n\n\n* **:src** 图片上传的URL。\n\n\n\n**JS部分**\n\n```javascript\n//设置全局表单提交格式\nVue.http.options.emulateJSON = true;\n\nnew Vue({\n    el: \'#app\',\n    data(){\n        return{\n            //文件上传的参数\n            dialogImageUrl: \'\',\n            dialogVisible: false,\n            //图片列表（用于在上传组件中回显图片）\n            fileList: [{name: \'\', url: \'\'}],\n        }\n    },\n    methods(){\n   		//文件上传成功的钩子函数\n        handleSuccess(res, file) {\n            this.$message({\n                type: \'info\',\n                message: \'图片上传成功\',\n                duration: 6000\n            });\n            if (file.response.success) {\n                this.editor.picture = file.response.message; //将返回的文件储存路径赋值picture字段\n            }\n        },\n        //删除文件之前的钩子函数\n        handleRemove(file, fileList) {\n            this.$message({\n                type: \'info\',\n                message: \'已删除原有图片\',\n                duration: 6000\n            });\n        },\n        //点击列表中已上传的文件事的钩子函数\n        handlePreview(file) {\n        },\n        //上传的文件个数超出设定时触发的函数\n        onExceed(files, fileList) {\n            this.$message({\n                type: \'info\',\n                message: \'最多只能上传一个图片\',\n                duration: 6000\n            });\n        },\n        //文件上传前的前的钩子函数\n        //参数是上传的文件，若返回false，或返回Primary且被reject，则停止上传\n        beforeUpload(file) {\n            const isJPG = file.type === \'image/jpeg\';\n            const isGIF = file.type === \'image/gif\';\n            const isPNG = file.type === \'image/png\';\n            const isBMP = file.type === \'image/bmp\';\n            const isLt2M = file.size / 1024 / 1024 < 2;\n\n            if (!isJPG && !isGIF && !isPNG && !isBMP) {\n                this.$message.error(\'上传图片必须是JPG/GIF/PNG/BMP 格式!\');\n            }\n            if (!isLt2M) {\n                this.$message.error(\'上传图片大小不能超过 2MB!\');\n            }\n            return (isJPG || isBMP || isGIF || isPNG) && isLt2M;\n        },     \n    }\n});\n```\n\n**解释**\n\n如上的JS代码，主要是定义一些钩子函数，这里我么里梳理一下逻辑：\n\n1、点击ElementUI的上传组件，浏览器自动弹出文件上传选择窗口，我们选择要上传的图片。\n\n2、选择好了要上传的图片，点击弹窗右下角的确定按钮触发JS中定义的钩子函数。\n\n3、首先触发的钩子函数是`beforeUpload(file)`函数，其中的参数`file`即代表当前上传的文件对象，`beforeUpload()`定义了对上传文件格式校验。如果不是允许的格式就弹出错误信息，并阻止文件上传，若我那件格式允许，则继续执行。\n\n4、通过了`beforeUpload()`函数的校验，文件开始调用后端接口将数据发送给后端。文件的字段名：`picture`，格式：`multipart/form-data`，虽然我们的表单没有定义`enctype=\"multipart/form-data\"`属性，但是HTTP请求头会自动设置为`multipart/form-data`类型。\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-dcd28bddc31a63de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n5、这时，如果后端逻辑没有错误，已经正常的将图片上传到服务器上了，可以在指定文件夹中查看到已上传的图片，那么此时JS中会自动调用`handleSuccess()`钩子函数，因为我们设置后端上传接口上传成功返回的数据是文件的保存路径：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-fce7f2c140ed4474.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n那我们就将这个路径通过Vue的双向绑定，赋值给表单对象的字段`picture`，那么提交表单的时候，该字段对应的值就是这个路径了。\n\n6、如果我们再点击上传文件按钮，就会触发`onExceed()`函数，因为我们设置的`limit`最多上传一个。\n\n7、如果点击图片中的删除按钮，就会触发`handleRemove()`函数，并删除此图片。\n\n8、如果点击了已上传的文件列表，就会触发`handlePreview()`函数。\n\n\n\n## 实现表单提交\n\n表单提交就比较简单了，就是触发对应的click事件，触发其中定义的函数，将已在`data(){}`中定义的表单数据发送给后端接口：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-05466c45309a29e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n提交数据：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-3d038bd0378c8c57.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n**后端接口**\n\n```java\n@RequestMapping(\"/save\")\npublic Result save(Instrument instrument) {\n    if(instrument != null){\n        try{\n            instrumentService.save(instrument);\n            return new Result(true,\"添加成功\");\n        }catch (Exception e){\n            e.printStackTrace();\n        }\n    }\n    return new Result(false, \"发生未知错误\");\n}\n```\n\n数据库中保存的数据：\n\n![image](http://upload-images.jianshu.io/upload_images/12613204-31c0ab4e6f42d8db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n\n\n\n<br/>\n\n# 实现table回显图片\n\ntable回显图片也是很简单的，仅需要在列中增加一列：\n\n```html\n<el-table :data=\"instrument\">\n    <el-table-column label=\"图片\" width=\"130\">\n        <template scope=\"scope\">\n            <img :src=\"scope.row.picture\" class=\"picture\"/>\n        </template>\n    </el-table-column>\n    <el-table-column\n         label=\"运行状态\"\n         width=\"80\"\n         prop=\"operatingStatus\">\n    </el-table-column>\n</el-table>\n```\n\n因为使用Vue，根据其双向绑定的思想，再结合Element-UI提供渲染表格的方式是在`<el-table>`的`:data`中指定对应要渲染的数据即可。\n\n**注意** ElementUI渲染table的方式是：1、`<el-table>`中定义`:data`；2、`<el-table-column>`中定义`prop=\"data中的参数\"`。但是因为我们要显示的是图片而不是文本数据，所以要在`<img>`中定义`:src=\"data中的变量\"`即可实现渲染。\n\n<br/>\n\n**后端**就是正常的查询数据库数据即可了，为什么数据库中保存了这个URL图片就能直接显示到HTML中，请看我这篇博文： [SpringMVC实现文件上传和下载](http://tycoding.cn/2018/05/31/Spring-6/) \n\n\n\n\n\n\n\n\n\n\n\n\n\n<br/>\n\n# 交流\n\n如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。博主目前一直在自学JAVA中，技术有限，如果可以，会尽力给大家提供一些帮助，或是一些学习方法，当然群里的大佬都会积极给新手答疑的。所以，别犹豫，快来加入我们吧！\n\n<br/>\n\n# 联系\n\nIf you have some questions after you see this article, you can contact me or you can find some info by clicking these links.\n\n- [Blog@TyCoding\'s blog](http://www.tycoding.cn)\n- [GitHub@TyCoding](https://github.com/TyCoding)\n- [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)', NULL, 'http://tycoding.cn', '1', 2453, '2018-02-01 00:00:01', '2018-02-01 00:00:01', '2018-11-02 12:47:25', 0);
COMMIT;

-- ----------------------------
-- Table structure for tb_article_category
-- ----------------------------
DROP TABLE IF EXISTS `tb_article_category`;
CREATE TABLE `tb_article_category` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `article_id` bigint(20) NOT NULL COMMENT '文章ID',
  `category_id` bigint(20) NOT NULL COMMENT '分类ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='文章&&分类关联表';

-- ----------------------------
-- Records of tb_article_category
-- ----------------------------
BEGIN;
INSERT INTO `tb_article_category` VALUES (1, 1, 4);
INSERT INTO `tb_article_category` VALUES (2, 2, 2);
INSERT INTO `tb_article_category` VALUES (3, 4, 3);
INSERT INTO `tb_article_category` VALUES (4, 5, 1);
INSERT INTO `tb_article_category` VALUES (5, 6, 1);
COMMIT;

-- ----------------------------
-- Table structure for tb_article_tags
-- ----------------------------
DROP TABLE IF EXISTS `tb_article_tags`;
CREATE TABLE `tb_article_tags` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `article_id` bigint(20) NOT NULL COMMENT '文章ID',
  `tag_id` bigint(20) NOT NULL COMMENT '标签ID',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 COMMENT='文章&&标签关联表';

-- ----------------------------
-- Records of tb_article_tags
-- ----------------------------
BEGIN;
INSERT INTO `tb_article_tags` VALUES (1, 1, 1);
INSERT INTO `tb_article_tags` VALUES (2, 1, 1);
INSERT INTO `tb_article_tags` VALUES (3, 1, 1);
INSERT INTO `tb_article_tags` VALUES (5, 1, 4);
INSERT INTO `tb_article_tags` VALUES (6, 2, 4);
INSERT INTO `tb_article_tags` VALUES (7, 4, 5);
INSERT INTO `tb_article_tags` VALUES (8, 4, 4);
INSERT INTO `tb_article_tags` VALUES (9, 5, 4);
INSERT INTO `tb_article_tags` VALUES (10, 6, 1);
INSERT INTO `tb_article_tags` VALUES (11, 7, 5);
INSERT INTO `tb_article_tags` VALUES (15, 11, 5);
INSERT INTO `tb_article_tags` VALUES (16, 11, 4);
COMMIT;

-- ----------------------------
-- Table structure for tb_category
-- ----------------------------
DROP TABLE IF EXISTS `tb_category`;
CREATE TABLE `tb_category` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `name` varchar(100) DEFAULT NULL COMMENT '分类名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='分类表';

-- ----------------------------
-- Records of tb_category
-- ----------------------------
BEGIN;
INSERT INTO `tb_category` VALUES (1, '测试');
INSERT INTO `tb_category` VALUES (2, '随笔');
INSERT INTO `tb_category` VALUES (3, '心情');
INSERT INTO `tb_category` VALUES (4, 'springboot');
COMMIT;

-- ----------------------------
-- Table structure for tb_comments
-- ----------------------------
DROP TABLE IF EXISTS `tb_comments`;
CREATE TABLE `tb_comments` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `p_id` bigint(20) DEFAULT '0' COMMENT '父级ID，给哪个留言进行回复',
  `c_id` bigint(20) DEFAULT '0' COMMENT '子级ID，给哪个留言下的回复进行评论',
  `article_title` varchar(200) DEFAULT NULL COMMENT '文章标题',
  `article_id` bigint(20) DEFAULT NULL COMMENT '文章ID',
  `name` varchar(20) DEFAULT NULL COMMENT '昵称',
  `c_name` varchar(20) DEFAULT NULL COMMENT '给谁留言',
  `time` datetime NOT NULL COMMENT '留言时间',
  `content` text COMMENT '留言内容',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `url` varchar(200) DEFAULT NULL COMMENT '网址',
  `type` bigint(20) DEFAULT '0' COMMENT '分类：0:默认，文章详情页，1:友链页，2:关于页',
  `ip` varchar(20) DEFAULT NULL COMMENT 'IP地址',
  `device` varchar(100) DEFAULT NULL COMMENT '设备',
  `address` varchar(100) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8 COMMENT='评论表';

-- ----------------------------
-- Records of tb_comments
-- ----------------------------
BEGIN;
INSERT INTO `tb_comments` VALUES (106, 105, NULL, NULL, NULL, 'tycoding', NULL, '2019-03-26 08:14:31', '回复你', '122@qq.com', 'https://tycoding.cn', 2, NULL, 'Chrome,Mac OS X', NULL);
INSERT INTO `tb_comments` VALUES (107, 0, 0, NULL, NULL, 't2', NULL, '2019-03-26 08:42:09', '留言', '122@qq.com', 'https://tycoding.cn', 2, '127.0.0.1', 'Chrome,Mac OS X', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_comments` VALUES (108, 0, 0, '测试heiehi', 12, 'tycoding', NULL, '2019-03-26 18:28:31', '测试下', '1222@qq.com', 'http://tycoding.cn', 0, '127.0.0.1', 'Chrome,Mac OS X', '内网IP|0|0|内网IP|内网IP');
COMMIT;

-- ----------------------------
-- Table structure for tb_links
-- ----------------------------
DROP TABLE IF EXISTS `tb_links`;
CREATE TABLE `tb_links` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `name` varchar(100) DEFAULT NULL COMMENT '连接名称',
  `url` varchar(200) DEFAULT NULL COMMENT '连接URL',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='友链表';

-- ----------------------------
-- Records of tb_links
-- ----------------------------
BEGIN;
INSERT INTO `tb_links` VALUES (1, 'Tycoding\'s blog', 'http://tycoding.cn');
INSERT INTO `tb_links` VALUES (3, 'TyCodingAdvantage', 'http://study.tycoding.cn');
COMMIT;

-- ----------------------------
-- Table structure for tb_log
-- ----------------------------
DROP TABLE IF EXISTS `tb_log`;
CREATE TABLE `tb_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `username` varchar(20) DEFAULT NULL COMMENT '操作用户',
  `operation` varchar(20) DEFAULT NULL COMMENT '操作描述',
  `time` bigint(20) DEFAULT NULL COMMENT '耗时(毫秒)',
  `method` varchar(100) DEFAULT NULL COMMENT '操作方法',
  `params` varchar(255) DEFAULT NULL COMMENT '操作参数',
  `ip` varchar(20) DEFAULT NULL COMMENT 'IP地址',
  `create_time` datetime DEFAULT NULL COMMENT '操作时间',
  `location` varchar(20) DEFAULT NULL COMMENT '操作地点',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8 COMMENT='系统日志表';

-- ----------------------------
-- Records of tb_log
-- ----------------------------
BEGIN;
INSERT INTO `tb_log` VALUES (25, 'tycoding', '删除分类', 50, 'cn.tycoding.admin.controller.CategoryController.delete()', ' ids\"[5]\"', '127.0.0.1', '2019-03-26 21:02:51', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (26, 'tycoding', '删除分类', 16, 'cn.tycoding.admin.controller.CategoryController.delete()', ' ids\"[6]\"', '127.0.0.1', '2019-03-26 21:02:53', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (27, 'tycoding', '删除标签', 33, 'cn.tycoding.admin.controller.TagsControllers.delete()', ' ids\"[7]\"', '127.0.0.1', '2019-03-26 21:02:58', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (28, 'tycoding', '删除标签', 11, 'cn.tycoding.admin.controller.TagsControllers.delete()', ' ids\"[9]\"', '127.0.0.1', '2019-03-26 21:03:00', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (29, 'tycoding', '删除标签', 16, 'cn.tycoding.admin.controller.TagsControllers.delete()', ' ids\"[10]\"', '127.0.0.1', '2019-03-26 21:03:03', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (30, 'tycoding', '删除标签', 14, 'cn.tycoding.admin.controller.TagsControllers.delete()', ' ids\"[2]\"', '127.0.0.1', '2019-03-26 21:03:07', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (31, 'tycoding', '删除标签', 13, 'cn.tycoding.admin.controller.TagsControllers.delete()', ' ids\"[3]\"', '127.0.0.1', '2019-03-26 21:03:10', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (32, 'tycoding', '更新标签', 5, 'cn.tycoding.admin.controller.TagsControllers.update()', ' tag\"Tags(id=1, name=随笔, count=null)\"', '127.0.0.1', '2019-03-26 21:03:17', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (33, 'tycoding', '删除文章', 15, 'cn.tycoding.admin.controller.ArticleController.delete()', ' ids\"[10]\"', '127.0.0.1', '2019-03-26 21:03:32', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (34, 'tycoding', '删除文章', 15, 'cn.tycoding.admin.controller.ArticleController.delete()', ' ids\"[9]\"', '127.0.0.1', '2019-03-26 21:03:35', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (35, 'tycoding', '删除文章', 16, 'cn.tycoding.admin.controller.ArticleController.delete()', ' ids\"[8]\"', '127.0.0.1', '2019-03-26 21:03:37', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (36, 'tycoding', '删除文章', 16, 'cn.tycoding.admin.controller.ArticleController.delete()', ' ids\"[14]\"', '127.0.0.1', '2019-03-26 21:03:41', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (37, 'tycoding', '删除文章', 72, 'cn.tycoding.admin.controller.ArticleController.delete()', ' ids\"[12]\"', '127.0.0.1', '2019-03-26 21:03:43', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (38, 'tycoding', '更新系统设置', 7, 'cn.tycoding.admin.controller.UserController.updateSetting()', ' setting\"Setting(id=1, siteName=null, siteLinks=null, siteDonation=[{\\\"key\\\":\\\"alipay\\\",\\\"value\\\":\\\"/upload/1110530177678106624.png\\\"},{\\\"key\\\":\\\"wechat\\\",\\\"value\\\":\\\"/upload/1110478190190243840.png\\\"}], siteMusic=null, about=null, aboutMd=null)\"', '127.0.0.1', '2019-03-26 21:13:23', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (39, 'tycoding', '更新系统设置', 4, 'cn.tycoding.admin.controller.UserController.updateSetting()', ' setting\"Setting(id=1, siteName=null, siteLinks=null, siteDonation=[{\\\"key\\\":\\\"alipay\\\",\\\"value\\\":\\\"/upload/1110530177678106624.png\\\"},{\\\"key\\\":\\\"wechat\\\",\\\"value\\\":\\\"/upload/1110530236096372736.png\\\"}], siteMusic=null, about=null, aboutMd=null)\"', '127.0.0.1', '2019-03-26 21:13:36', '内网IP|0|0|内网IP|内网IP');
INSERT INTO `tb_log` VALUES (40, 'tycoding', '删除文章', 48, 'cn.tycoding.admin.controller.ArticleController.delete()', ' ids\"[11]\"', '127.0.0.1', '2019-03-28 18:38:45', '内网IP|0|0|内网IP|内网IP');
COMMIT;

-- ----------------------------
-- Table structure for tb_login_log
-- ----------------------------
DROP TABLE IF EXISTS `tb_login_log`;
CREATE TABLE `tb_login_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `username` varchar(20) DEFAULT NULL COMMENT '用户名',
  `ip` varchar(20) DEFAULT NULL COMMENT 'IP地址',
  `location` varchar(255) DEFAULT NULL COMMENT '登录地点',
  `create_time` datetime DEFAULT NULL COMMENT '登录时间',
  `device` varchar(255) DEFAULT NULL COMMENT '登录设备',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_login_log
-- ----------------------------
BEGIN;
INSERT INTO `tb_login_log` VALUES (26, 'tycoding', '127.0.0.1', '内网IP|0|0|内网IP|内网IP', '2019-03-26 20:49:15', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36');
INSERT INTO `tb_login_log` VALUES (27, 'tycoding', '127.0.0.1', '内网IP|0|0|内网IP|内网IP', '2019-03-26 21:13:00', 'Chrome -- Mac OS X');
INSERT INTO `tb_login_log` VALUES (28, 'tycoding', '127.0.0.1', '内网IP|0|0|内网IP|内网IP', '2019-03-28 18:38:33', 'Chrome -- Mac OS X');
INSERT INTO `tb_login_log` VALUES (29, 'tycoding', '127.0.0.1', '内网IP|0|0|内网IP|内网IP', '2019-03-28 19:18:52', 'Chrome -- Mac OS X');
INSERT INTO `tb_login_log` VALUES (30, 'tycoding', '127.0.0.1', '内网IP|0|0|内网IP|内网IP', '2019-03-28 19:21:04', 'Chrome -- Mac OS X');
INSERT INTO `tb_login_log` VALUES (31, 'tycoding', '127.0.0.1', '内网IP|0|0|内网IP|内网IP', '2019-03-28 19:30:36', 'Chrome -- Mac OS X');
COMMIT;

-- ----------------------------
-- Table structure for tb_setting
-- ----------------------------
DROP TABLE IF EXISTS `tb_setting`;
CREATE TABLE `tb_setting` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `site_name` varchar(200) DEFAULT NULL COMMENT '网站名称',
  `site_links` text COMMENT '社交链接，JSON格式',
  `site_donation` varchar(400) DEFAULT NULL COMMENT '捐赠，微信、支付宝收款图片，JSON格式',
  `site_music` varchar(100) DEFAULT NULL COMMENT '音乐ID',
  `about` text COMMENT '关于我，HTML格式',
  `about_md` text COMMENT '关于我，Markdown格式',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tb_setting
-- ----------------------------
BEGIN;
INSERT INTO `tb_setting` VALUES (1, 'TyCoding\'s blog', '[{\"key\": \"Github\", value: \"https://github.com/TyCoding\"}, {\"key\": \"知乎\", \"value\": \"https://www.zhihu.com/people/tomo-83-82/activities\"}]', '[{\"key\":\"支付宝\",\"value\":\"/site/images/alipay.png\"},{\"key\":\"微信支付\",\"value\":\"/site/images/wechat.png\"}]', '453843751', '<hr>\n<h2 id=\"h2-u672Cu4EBA\"><a name=\"本人\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>本人</h2><p>2017届大一在读</p>\n<p>技术很差劲、算法很差劲、数据结构很差劲</p>\n<p>目前一直在自学中，大一就过上了泡实验室的日子</p>\n<p>……（此处省略很多很多）</p>\n<p>希望有一天能改变这种现态吧！</p>\n<p>致此</p>\n<p>加油！</p>\n<hr>\n<h2 id=\"h2-u4EA4u6D41\"><a name=\"交流\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>交流</h2><p>如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。本人一直在自学java，目前技术有限，但如果可以的话会尽力帮助大家，希望能和大家共同进步。所以，快来加入我们吧！</p>\n<p><br/></p>\n<h2 id=\"h2-u8054u7CFB\"><a name=\"联系\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>联系</h2><p>If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.</p>\n<ul>\n<li><a href=\"http://www.tycoding.cn\"\">Blog&#64;TyCoding’s blog</a></li><li><a href=\"https://github.com/TyCoding\"\">GitHub&#64;TyCoding</a></li><li><a href=\"https://www.zhihu.com/people/tomo-83-82/activities\"\">ZhiHu&#64;TyCoding</a></li></ul>\n', '\n---\n\n## 本人\n\n2017届大一在读\n\n技术很差劲、算法很差劲、数据结构很差劲\n\n目前一直在自学中，大一就过上了泡实验室的日子\n\n......（此处省略很多很多）\n\n希望有一天能改变这种现态吧！\n\n致此\n\n加油！\n\n----------\n\n## 交流\n\n如果大家有兴趣，欢迎大家加入我的Java交流群：671017003 ，一起交流学习Java技术。本人一直在自学java，目前技术有限，但如果可以的话会尽力帮助大家，希望能和大家共同进步。所以，快来加入我们吧！\n\n<br/>\n\n## 联系\n\nIf you have some questions after you see this article, you can contact me or you can find some info by clicking these links.\n\n- [Blog@TyCoding\'s blog](http://www.tycoding.cn)\n- [GitHub@TyCoding](https://github.com/TyCoding)\n- [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)\n');
COMMIT;

-- ----------------------------
-- Table structure for tb_tags
-- ----------------------------
DROP TABLE IF EXISTS `tb_tags`;
CREATE TABLE `tb_tags` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `name` varchar(100) DEFAULT NULL COMMENT '标签名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='标签表';

-- ----------------------------
-- Records of tb_tags
-- ----------------------------
BEGIN;
INSERT INTO `tb_tags` VALUES (1, '随笔');
INSERT INTO `tb_tags` VALUES (4, '测试');
INSERT INTO `tb_tags` VALUES (5, '博客日志');
COMMIT;

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `username` varchar(100) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '密码',
  `salt` varchar(200) NOT NULL COMMENT '盐值',
  `avatar` varchar(200) DEFAULT NULL COMMENT '头像',
  `introduce` varchar(100) DEFAULT NULL COMMENT '介绍',
  `remark` varchar(100) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户表';

-- ----------------------------
-- Records of tb_user
-- ----------------------------
BEGIN;
INSERT INTO `tb_user` VALUES (1, 'tycoding', '5f9059b3feff398c928c7c1239e64975', 'afbe4bd05b55b755d2a3e7df3bc25586', '/img/avatar/default.jpg', '兴趣使然的Coder', '银河街角，时光路口');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
